├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── README.md ├── demo ├── hod-auth.js ├── hod-button.js ├── hod-create-container.js ├── hod-toolbar.js ├── index.html ├── package-lock.json ├── package.json ├── scripts.js ├── store.js └── web_modules │ ├── @adobe │ └── lit-mobx.js │ ├── @lrnwebcomponents │ ├── hax-logo.js │ └── simple-login.js │ ├── @vaadin │ └── router.js │ ├── import-map.json │ ├── lit-element.js │ ├── mobx.js │ └── shader-doodle.js ├── docker-compose-traefik-v1.yml ├── docker-compose.yml ├── package.json ├── prisma ├── migrations │ ├── 20191002140626-initial │ │ ├── README.md │ │ ├── schema.prisma │ │ └── steps.json │ └── lift.lock └── schema.prisma ├── seed.ts ├── server └── index.js ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_CLIENT_ID=xxxxxxxxxxxxxxxxxxxx 2 | GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 3 | GITHUB_SCOPE=user:email,read:user,public_rep 4 | PRISMA_DB_PROVIDER=postgresql 5 | PRISMA_DB_URL=postgresql://prisma:prisma@postgres/ 6 | HAXCMS_OAUTH_JWT_SECRET=abc123 #shared secret with other microservices that need to decode the jwt 7 | HAXCMS_OAUTH_JWT_REFRESH_SECRET=zyx456 8 | FQDN=http://auth.haxcms.localhost 9 | SCOPE=haxcms.localhost -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | WORKDIR /home/node/app 4 | 5 | COPY package.json yarn.lock ./ 6 | RUN yarn --pure-lockfile --no-cache 7 | 8 | RUN chown -R node:node node_modules 9 | RUN chown -R node:node /tmp 10 | COPY --chown=node:node . . 11 | 12 | USER node 13 | 14 | CMD [ "yarn", "start" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAXcms Github Oauth Microservice 2 | 3 | This is a standalone microservice that will login a user via Github OAuth, 4 | store create a user in this auth service db, issue a jwt, store it in a Cookie, 5 | and redirect the user back to the requesting page. 6 | 7 | ## Development 8 | 9 | Create a Github Oauth App 10 | 11 | ```bash 12 | cp .env.example .env 13 | ``` 14 | 15 | Add your credentials to the .env file 16 | 17 | ```bash 18 | docker-compose up --build 19 | ``` 20 | 21 | Visit `http://haxcms.localhost/login` 22 | -------------------------------------------------------------------------------- /demo/hod-auth.js: -------------------------------------------------------------------------------- 1 | import { html } from "../../web_modules/lit-element.js"; 2 | import { MobxLitElement } from "../../web_modules/@adobe/lit-mobx.js"; 3 | import { store } from './store.js' 4 | 5 | class HodAuth extends MobxLitElement { 6 | constructor() { 7 | super(); 8 | this.store = store 9 | } 10 | async connectedCallback() { 11 | super.connectedCallback(); 12 | 13 | await this.getAccessToken(); 14 | await this.getUser(); 15 | } 16 | 17 | async getAccessToken() { 18 | try { 19 | const access_token = await fetch( 20 | "http://auth.haxcms.localhost/access_token", 21 | { 22 | credentials: "include" 23 | } 24 | ); 25 | if (access_token.status === 200) { 26 | window.localStorage.setItem("access_token", await access_token.json()); 27 | return await access_token.json(); 28 | } 29 | } catch (error) {} 30 | } 31 | 32 | async getUser() { 33 | if (typeof window.localStorage.access_token !== "undefined") { 34 | const access_token = window.localStorage.access_token 35 | const user = await fetch("http://auth.haxcms.localhost/graphql", { 36 | method: "POST", 37 | headers: { 38 | Accept: "application/json", 39 | "Content-Type": "application/json", 40 | Authorization: `Bearer ${access_token}` 41 | }, 42 | body: ' \ 43 | { \ 44 | "query": "query { user { name }}" \ 45 | }' 46 | }).then(res => res.json()); 47 | 48 | this.store.name = user.data.user.name 49 | } 50 | } 51 | 52 | } 53 | customElements.define("hod-auth", HodAuth); 54 | -------------------------------------------------------------------------------- /demo/hod-button.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from "../../web_modules/lit-element.js"; 2 | import { MobxLitElement } from "../../web_modules/@adobe/lit-mobx.js"; 3 | 4 | class HodButton extends MobxLitElement { 5 | static get properties() { 6 | } 7 | 8 | constructor() { 9 | super(); 10 | } 11 | 12 | render() { 13 | return html` 14 | `; 15 | } 16 | } 17 | customElements.define("hod-button", HodButton); 18 | -------------------------------------------------------------------------------- /demo/hod-create-container.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from "../../web_modules/lit-element.js"; 2 | import { MobxLitElement } from "../../web_modules/@adobe/lit-mobx.js"; 3 | import { store } from "./store.js"; 4 | 5 | class HodCreateContainer extends MobxLitElement { 6 | constructor() { 7 | super(); 8 | this.store = store; 9 | } 10 | 11 | render() { 12 | return html` 13 | 40 | ${this.store.name 41 | ? this.store.createContainer.state === "default" 42 | ? html` 43 |
44 | 45 |
46 | ` 47 | : this.store.createContainer.state === "initialized" 48 | ? html` 49 |
50 | 51 |
52 | ` 53 | : "" 54 | : ""} 55 | `; 56 | } 57 | 58 | __clicked(e) { 59 | this.store.createContainer.state = "initialized"; 60 | } 61 | } 62 | customElements.define("hod-create-container", HodCreateContainer); 63 | -------------------------------------------------------------------------------- /demo/hod-toolbar.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from "../../web_modules/lit-element.js"; 2 | import { MobxLitElement } from "../../web_modules/@adobe/lit-mobx.js"; 3 | import { store } from "./store.js" 4 | import "../../web_modules/@lrnwebcomponents/hax-logo.js"; 5 | 6 | class HodToolbar extends MobxLitElement { 7 | constructor() { 8 | super(); 9 | this.store = store 10 | } 11 | 12 | connectedCallback() { 13 | super.connectedCallback() 14 | } 15 | 16 | render() { 17 | return html` 18 | 53 | 56 |
57 | 58 |
${this.store.name}
59 |
60 | ` 61 | } 62 | } 63 | customElements.define("hod-toolbar", HodToolbar); 64 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | HAXcms On Demand 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@adobe/lit-mobx": { 8 | "version": "0.0.2", 9 | "resolved": "https://registry.npmjs.org/@adobe/lit-mobx/-/lit-mobx-0.0.2.tgz", 10 | "integrity": "sha512-OAA+Ji0ei5GBv6uNWPJCs1YN/WmCnOM0mPDFiB2iKLQPVi5/bbfcuPpzrJQR5n7lhFZiojh18dGGVd0pKPtEbg==" 11 | }, 12 | "@material/animation": { 13 | "version": "3.1.0", 14 | "resolved": "https://registry.npmjs.org/@material/animation/-/animation-3.1.0.tgz", 15 | "integrity": "sha512-ZfP95awrPBLhpaCTPNx+xKYPp2D88fzf5p5YNVp6diUAGRpq3g12Aq7qJfIHDXAFo5CtrYhgOKRqIKxUVcFisQ==", 16 | "requires": { 17 | "tslib": "^1.9.3" 18 | } 19 | }, 20 | "@material/base": { 21 | "version": "3.1.0", 22 | "resolved": "https://registry.npmjs.org/@material/base/-/base-3.1.0.tgz", 23 | "integrity": "sha512-pWEBHyPrMV3rdnjqWWH96h5t3MxQI6V1J9jOor+UBG7bXQtr6InTabTqhz5CLY7r+qZU8YvNh2OKIy8heP0cyQ==", 24 | "requires": { 25 | "tslib": "^1.9.3" 26 | } 27 | }, 28 | "@material/button": { 29 | "version": "3.2.0", 30 | "resolved": "https://registry.npmjs.org/@material/button/-/button-3.2.0.tgz", 31 | "integrity": "sha512-VEASy3Dtc7BCo8/cuUIp6w0+/l4U1myGZffK5GeFVInP/erStSQOmYXT7jGXkZpUglRzWOpVvEpc6nsvhMqGbw==", 32 | "requires": { 33 | "@material/elevation": "^3.1.0", 34 | "@material/feature-targeting": "^3.1.0", 35 | "@material/ripple": "^3.2.0", 36 | "@material/rtl": "^3.2.0", 37 | "@material/shape": "^3.1.0", 38 | "@material/theme": "^3.1.0", 39 | "@material/typography": "^3.1.0" 40 | } 41 | }, 42 | "@material/dom": { 43 | "version": "3.1.0", 44 | "resolved": "https://registry.npmjs.org/@material/dom/-/dom-3.1.0.tgz", 45 | "integrity": "sha512-RtBLSkrBjMfHwknaGBifAIC8cBWF9pXjz2IYqfI2braB6SfQI4vhdJviwyiA5BmA/psn3cKpBUZbHI0ym0O0SQ==", 46 | "requires": { 47 | "tslib": "^1.9.3" 48 | } 49 | }, 50 | "@material/elevation": { 51 | "version": "3.1.0", 52 | "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-3.1.0.tgz", 53 | "integrity": "sha512-e45LqiG6LfbR1M52YkSLA7pQPeYJOuOVzLp27xy2072TnLuJexMxhEaG4O1novEIjsTtMjqfrfJ/koODU5vEew==", 54 | "requires": { 55 | "@material/animation": "^3.1.0", 56 | "@material/feature-targeting": "^3.1.0", 57 | "@material/theme": "^3.1.0" 58 | } 59 | }, 60 | "@material/feature-targeting": { 61 | "version": "3.1.0", 62 | "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-3.1.0.tgz", 63 | "integrity": "sha512-aXAa1Pv6w32URacE9LfMsl9zI6hFwx1K0Lp3Xpyf4rAkmaAB6z0gOkhicOrVFc0f64YheJgHjE7hJFieVenQdw==" 64 | }, 65 | "@material/ripple": { 66 | "version": "3.2.0", 67 | "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-3.2.0.tgz", 68 | "integrity": "sha512-GtwkfNakALmfGLs6TpdFIVeAWjRqbyT7WfEw9aU7elUokABfHES+O0KoSKQSMQiSQ8Vjl90MONzNsN1Evi/1YQ==", 69 | "requires": { 70 | "@material/animation": "^3.1.0", 71 | "@material/base": "^3.1.0", 72 | "@material/dom": "^3.1.0", 73 | "@material/feature-targeting": "^3.1.0", 74 | "@material/theme": "^3.1.0", 75 | "tslib": "^1.9.3" 76 | } 77 | }, 78 | "@material/rtl": { 79 | "version": "3.2.0", 80 | "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-3.2.0.tgz", 81 | "integrity": "sha512-L/w9m9Yx1ceOw/VjEfeJoqD4rW9QP3IBb9MamXAg3qUi/zsztoXD/FUw179pxkLn4huFFNlVYZ4Y1y6BpM0PMA==" 82 | }, 83 | "@material/shape": { 84 | "version": "3.1.0", 85 | "resolved": "https://registry.npmjs.org/@material/shape/-/shape-3.1.0.tgz", 86 | "integrity": "sha512-Oyvs7YjHfByA0e9IVVp7ojAlPwgSu3Bl0cioiE0OdkidkAaNu0izM2ryRzMBDH5o8+lRD0kpZoT+9CVVCdaYIg==", 87 | "requires": { 88 | "@material/feature-targeting": "^3.1.0" 89 | } 90 | }, 91 | "@material/theme": { 92 | "version": "3.1.0", 93 | "resolved": "https://registry.npmjs.org/@material/theme/-/theme-3.1.0.tgz", 94 | "integrity": "sha512-N4JX+akOwg1faAvFvIEhDcwW4cZfUpwEn8lct6Vs3WczjLF6/KdIoLVaYh+eVl1bzfsoIuWvx56j0B1PjXZw9g==", 95 | "requires": { 96 | "@material/feature-targeting": "^3.1.0" 97 | } 98 | }, 99 | "@material/typography": { 100 | "version": "3.1.0", 101 | "resolved": "https://registry.npmjs.org/@material/typography/-/typography-3.1.0.tgz", 102 | "integrity": "sha512-aSNBQvVxIH1kORSYdLGuSTivx6oJ1MSOSTUAsUwhXPQLQlvbdFeZaqUp7xgn+EvRsHGRFhWk5YGuiBds9+7zQg==", 103 | "requires": { 104 | "@material/feature-targeting": "^3.1.0" 105 | } 106 | }, 107 | "lit-element": { 108 | "version": "2.2.1", 109 | "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.2.1.tgz", 110 | "integrity": "sha512-ipDcgQ1EpW6Va2Z6dWm79jYdimVepO5GL0eYkZrFvdr0OD/1N260Q9DH+K5HXHFrRoC7dOg+ZpED2XE0TgGdXw==", 111 | "requires": { 112 | "lit-html": "^1.0.0" 113 | } 114 | }, 115 | "lit-html": { 116 | "version": "1.1.2", 117 | "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.1.2.tgz", 118 | "integrity": "sha512-FFlUMKHKi+qG1x1iHNZ1hrtc/zHmfYTyrSvs3/wBTvaNtpZjOZGWzU7efGYVpgp6KvWeKF6ql9/KsCq6Z/mEDA==" 119 | }, 120 | "mobx": { 121 | "version": "5.13.1", 122 | "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.13.1.tgz", 123 | "integrity": "sha512-v29wXJeOw3GpnfDxDAecHdnoBZzTrKju6UZoQxcUDiNHyyo3dBWCkDOiTsmRLf7+VXDfbmnUh9P+XGqXWVOW7Q==" 124 | }, 125 | "tslib": { 126 | "version": "1.10.0", 127 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 128 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "hod-auth.js", 6 | "scripts": { 7 | "postinstall": "npx @pika/web" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@adobe/lit-mobx": "^0.0.2", 14 | "lit-element": "^2.2.1", 15 | "mobx": "^5.13.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/scripts.js: -------------------------------------------------------------------------------- 1 | import './hod-auth.js' 2 | import './hod-toolbar.js' 3 | import './hod-create-container.js' -------------------------------------------------------------------------------- /demo/store.js: -------------------------------------------------------------------------------- 1 | import { 2 | observable, 3 | decorate, 4 | computed, 5 | autorun, 6 | action, 7 | toJS 8 | } from "../../web_modules/mobx.js"; 9 | 10 | class Store { 11 | constructor() { 12 | this.name = null; 13 | this.createContainer = { 14 | state: 'default' 15 | } 16 | } 17 | } 18 | 19 | decorate(Store, { 20 | name: observable, 21 | createContainer: observable 22 | }); 23 | 24 | export const store = new Store() -------------------------------------------------------------------------------- /demo/web_modules/@adobe/lit-mobx.js: -------------------------------------------------------------------------------- 1 | import { Reaction } from '../mobx.js'; 2 | import { LitElement } from '../lit-element.js'; 3 | 4 | /* 5 | Copyright 2018 Adobe. All rights reserved. 6 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. You may obtain a copy 8 | of the License at http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software distributed under 11 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 12 | OF ANY KIND, either express or implied. See the License for the specific language 13 | governing permissions and limitations under the License. 14 | */ 15 | const reaction = Symbol('LitMobxRenderReaction'); 16 | const cachedRequestUpdate = Symbol('LitMobxRequestUpdate'); 17 | const cachedPerformUpdate = Symbol('LitMobxPerformUpdate'); 18 | /** 19 | * A class mixin which can be applied to lit-element's 20 | * [UpdatingElement](https://lit-element.polymer-project.org/api/classes/_lib_updating_element_.updatingelement.html) 21 | * derived classes. This mixin adds a mobx reaction which tracks the update method of UpdatingElement. 22 | * 23 | * Any observables used in the render template of the element will be tracked by a reaction 24 | * and cause an update of the element upon change. 25 | * 26 | * @param constructor the constructor to extend from to add the mobx reaction, must be derived from UpdatingElement. 27 | */ 28 | function MobxReactionUpdate(constructor) { 29 | var _a, _b, _c; 30 | return _c = class MobxReactingElement extends constructor { 31 | constructor() { 32 | super(...arguments); 33 | this[_a] = () => this.requestUpdate(); 34 | this[_b] = () => super.performUpdate(); 35 | } 36 | connectedCallback() { 37 | super.connectedCallback(); 38 | /* istanbul ignore next */ 39 | const name = this.constructor.name || this.nodeName; 40 | this[reaction] = new Reaction(`${name}.update()`, this[cachedRequestUpdate]); 41 | if (this.hasUpdated) 42 | this.requestUpdate(); 43 | } 44 | disconnectedCallback() { 45 | super.disconnectedCallback(); 46 | /* istanbul ignore else */ 47 | if (this[reaction]) { 48 | this[reaction].dispose(); 49 | this[reaction] = undefined; 50 | } 51 | } 52 | performUpdate() { 53 | if (this[reaction]) { 54 | this[reaction].track(this[cachedPerformUpdate]); 55 | } 56 | else { 57 | super.performUpdate(); 58 | } 59 | } 60 | }, 61 | _a = cachedRequestUpdate, 62 | _b = cachedPerformUpdate, 63 | _c; 64 | } 65 | 66 | /* 67 | Copyright 2018 Adobe. All rights reserved. 68 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 69 | you may not use this file except in compliance with the License. You may obtain a copy 70 | of the License at http://www.apache.org/licenses/LICENSE-2.0 71 | 72 | Unless required by applicable law or agreed to in writing, software distributed under 73 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 74 | OF ANY KIND, either express or implied. See the License for the specific language 75 | governing permissions and limitations under the License. 76 | */ 77 | /** 78 | * A LitElement derived class which uses the MobxReactionUpdate mixin to create a mobx 79 | * reactive implementation of LitElement. 80 | * 81 | * Any observables used in the render template of the element will be tracked by a reaction 82 | * and cause an update of the element upon change. 83 | */ 84 | class MobxLitElement extends MobxReactionUpdate(LitElement) { 85 | } 86 | 87 | export { MobxLitElement, MobxReactionUpdate }; 88 | -------------------------------------------------------------------------------- /demo/web_modules/@lrnwebcomponents/hax-logo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The Pennsylvania State University 3 | * @license Apache-2.0, see License.md for full text. 4 | */ 5 | /** 6 | * `hax-logo` 7 | * `logo element for hax, obviously as a hax capable element.` 8 | * 9 | * @microcopy - language worth noting: 10 | * - 11 | * 12 | * @customElement 13 | * @demo demo/index.html 14 | */ 15 | class HaxLogo extends HTMLElement { 16 | 17 | // render function 18 | get html() { 19 | return ` 20 | 104 | <h-a-x
the
web
>
`; 105 | } 106 | 107 | // haxProperty definition 108 | static get haxProperties() { 109 | return { 110 | "canScale": true, 111 | "canPosition": true, 112 | "canEditSource": false, 113 | "gizmo": { 114 | "title": "Hax logo", 115 | "description": "logo element for hax, obviously as a hax capable element.", 116 | "icon": "icons:android", 117 | "color": "green", 118 | "groups": ["Logo"], 119 | "handles": [ 120 | { 121 | "type": "todo:read-the-docs-for-usage" 122 | } 123 | ], 124 | "meta": { 125 | "author": "btopro", 126 | "owner": "The Pennsylvania State University" 127 | } 128 | }, 129 | "settings": { 130 | "quick": [], 131 | "configure": [ 132 | { 133 | "attribute": "size", 134 | "description": "Size of the HAX logo to place", 135 | "inputMethod": "select", 136 | "options": { 137 | "mini": "Mini", 138 | "small": "Small", 139 | "normal": "Normal", 140 | "large": "Large" 141 | }, 142 | "required": false 143 | }, 144 | { 145 | "attribute": "toupper", 146 | "description": "Whether to transform logo to upper case", 147 | "inputMethod": "boolean", 148 | "required": false 149 | } 150 | ], 151 | "advanced": [] 152 | } 153 | } 154 | ; 155 | } 156 | // properties available to the custom element for data binding 157 | static get properties() { 158 | let props = {} 159 | ; 160 | if (super.properties) { 161 | props = Object.assign(props, super.properties); 162 | } 163 | return props; 164 | } 165 | 166 | /** 167 | * Store the tag name to make it easier to obtain directly. 168 | * @notice function name must be here for tooling to operate correctly 169 | */ 170 | static get tag() { 171 | return "hax-logo"; 172 | } 173 | /** 174 | * life cycle 175 | */ 176 | constructor(delayRender = false) { 177 | super(); 178 | if (!window.__haxLogoFontLoaded) { 179 | let link = document.createElement("link"); 180 | link.setAttribute( 181 | "href", 182 | "https://fonts.googleapis.com/css?family=Press+Start+2P&display=swap" 183 | ); 184 | link.setAttribute("rel", "stylesheet"); 185 | document.head.appendChild(link); 186 | window.__haxLogoFontLoaded = true; 187 | } 188 | // set tag for later use 189 | this.tag = HaxLogo.tag; 190 | this.template = document.createElement("template"); 191 | 192 | this.attachShadow({ mode: "open" }); 193 | 194 | if (!delayRender) { 195 | this.render(); 196 | } 197 | } 198 | /** 199 | * life cycle, element is afixed to the DOM 200 | */ 201 | connectedCallback() { 202 | if (window.ShadyCSS) { 203 | window.ShadyCSS.styleElement(this); 204 | } 205 | } 206 | 207 | render() { 208 | this.shadowRoot.innerHTML = null; 209 | this.template.innerHTML = this.html; 210 | 211 | if (window.ShadyCSS) { 212 | window.ShadyCSS.prepareTemplate(this.template, this.tag); 213 | } 214 | this.shadowRoot.appendChild(this.template.content.cloneNode(true)); 215 | } 216 | get size() { 217 | return this.getAttribute("size"); 218 | } 219 | set size(newValue) { 220 | if (newValue) { 221 | this.setAttribute("size", newValue); 222 | } 223 | } 224 | 225 | get toupper() { 226 | return this.getAttribute("toupper"); 227 | } 228 | set toupper(newValue) { 229 | if (newValue) { 230 | this.setAttribute("toupper", "toupper"); 231 | } 232 | } 233 | } 234 | window.customElements.define(HaxLogo.tag, HaxLogo); 235 | 236 | export { HaxLogo }; 237 | -------------------------------------------------------------------------------- /demo/web_modules/@vaadin/router.js: -------------------------------------------------------------------------------- 1 | function toArray(objectOrArray) { 2 | objectOrArray = objectOrArray || []; 3 | return Array.isArray(objectOrArray) ? objectOrArray : [objectOrArray]; 4 | } 5 | 6 | function log(msg) { 7 | return `[Vaadin.Router] ${msg}`; 8 | } 9 | 10 | function logValue(value) { 11 | if (typeof value !== 'object') { 12 | return String(value); 13 | } 14 | 15 | const stringType = Object.prototype.toString.call(value).match(/ (.*)\]$/)[1]; 16 | if (stringType === 'Object' || stringType === 'Array') { 17 | return `${stringType} ${JSON.stringify(value)}`; 18 | } else { 19 | return stringType; 20 | } 21 | } 22 | 23 | const MODULE = 'module'; 24 | const NOMODULE = 'nomodule'; 25 | const bundleKeys = [MODULE, NOMODULE]; 26 | 27 | function ensureBundle(src) { 28 | if (!src.match(/.+\.[m]?js$/)) { 29 | throw new Error( 30 | log(`Unsupported type for bundle "${src}": .js or .mjs expected.`) 31 | ); 32 | } 33 | } 34 | 35 | function ensureRoute(route) { 36 | if (!route || !isString(route.path)) { 37 | throw new Error( 38 | log(`Expected route config to be an object with a "path" string property, or an array of such objects`) 39 | ); 40 | } 41 | 42 | const bundle = route.bundle; 43 | 44 | const stringKeys = ['component', 'redirect', 'bundle']; 45 | if ( 46 | !isFunction(route.action) && 47 | !Array.isArray(route.children) && 48 | !isFunction(route.children) && 49 | !isObject(bundle) && 50 | !stringKeys.some(key => isString(route[key])) 51 | ) { 52 | throw new Error( 53 | log( 54 | `Expected route config "${route.path}" to include either "${stringKeys.join('", "')}" ` + 55 | `or "action" function but none found.` 56 | ) 57 | ); 58 | } 59 | 60 | if (bundle) { 61 | if (isString(bundle)) { 62 | ensureBundle(bundle); 63 | } else if (!bundleKeys.some(key => key in bundle)) { 64 | throw new Error( 65 | log('Expected route bundle to include either "' + NOMODULE + '" or "' + MODULE + '" keys, or both') 66 | ); 67 | } else { 68 | bundleKeys.forEach(key => key in bundle && ensureBundle(bundle[key])); 69 | } 70 | } 71 | 72 | if (route.redirect) { 73 | ['bundle', 'component'].forEach(overriddenProp => { 74 | if (overriddenProp in route) { 75 | console.warn( 76 | log( 77 | `Route config "${route.path}" has both "redirect" and "${overriddenProp}" properties, ` + 78 | `and "redirect" will always override the latter. Did you mean to only use "${overriddenProp}"?` 79 | ) 80 | ); 81 | } 82 | }); 83 | } 84 | } 85 | 86 | function ensureRoutes(routes) { 87 | toArray(routes).forEach(route => ensureRoute(route)); 88 | } 89 | 90 | function loadScript(src, key) { 91 | let script = document.head.querySelector('script[src="' + src + '"][async]'); 92 | if (!script) { 93 | script = document.createElement('script'); 94 | script.setAttribute('src', src); 95 | if (key === MODULE) { 96 | script.setAttribute('type', MODULE); 97 | } else if (key === NOMODULE) { 98 | script.setAttribute(NOMODULE, ''); 99 | } 100 | script.async = true; 101 | } 102 | return new Promise((resolve, reject) => { 103 | script.onreadystatechange = script.onload = e => { 104 | script.__dynamicImportLoaded = true; 105 | resolve(e); 106 | }; 107 | script.onerror = e => { 108 | if (script.parentNode) { 109 | script.parentNode.removeChild(script); 110 | } 111 | reject(e); 112 | }; 113 | if (script.parentNode === null) { 114 | document.head.appendChild(script); 115 | } else if (script.__dynamicImportLoaded) { 116 | resolve(); 117 | } 118 | }); 119 | } 120 | 121 | function loadBundle(bundle) { 122 | if (isString(bundle)) { 123 | return loadScript(bundle); 124 | } else { 125 | return Promise.race( 126 | bundleKeys 127 | .filter(key => key in bundle) 128 | .map(key => loadScript(bundle[key], key)) 129 | ); 130 | } 131 | } 132 | 133 | function fireRouterEvent(type, detail) { 134 | return !window.dispatchEvent(new CustomEvent( 135 | `vaadin-router-${type}`, 136 | {cancelable: type === 'go', detail} 137 | )); 138 | } 139 | 140 | function isObject(o) { 141 | // guard against null passing the typeof check 142 | return typeof o === 'object' && !!o; 143 | } 144 | 145 | function isFunction(f) { 146 | return typeof f === 'function'; 147 | } 148 | 149 | function isString(s) { 150 | return typeof s === 'string'; 151 | } 152 | 153 | function getNotFoundError(context) { 154 | const error = new Error(log(`Page not found (${context.pathname})`)); 155 | error.context = context; 156 | error.code = 404; 157 | return error; 158 | } 159 | 160 | const notFoundResult = new (class NotFoundResult {})(); 161 | 162 | /* istanbul ignore next: coverage is calculated in Chrome, this code is for IE */ 163 | function getAnchorOrigin(anchor) { 164 | // IE11: on HTTP and HTTPS the default port is not included into 165 | // window.location.origin, so won't include it here either. 166 | const port = anchor.port; 167 | const protocol = anchor.protocol; 168 | const defaultHttp = protocol === 'http:' && port === '80'; 169 | const defaultHttps = protocol === 'https:' && port === '443'; 170 | const host = (defaultHttp || defaultHttps) 171 | ? anchor.hostname // does not include the port number (e.g. www.example.org) 172 | : anchor.host; // does include the port number (e.g. www.example.org:80) 173 | return `${protocol}//${host}`; 174 | } 175 | 176 | // The list of checks is not complete: 177 | // - SVG support is missing 178 | // - the 'rel' attribute is not considered 179 | function vaadinRouterGlobalClickHandler(event) { 180 | // ignore the click if the default action is prevented 181 | if (event.defaultPrevented) { 182 | return; 183 | } 184 | 185 | // ignore the click if not with the primary mouse button 186 | if (event.button !== 0) { 187 | return; 188 | } 189 | 190 | // ignore the click if a modifier key is pressed 191 | if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) { 192 | return; 193 | } 194 | 195 | // find the element that the click is at (or within) 196 | let anchor = event.target; 197 | const path = event.composedPath 198 | ? event.composedPath() 199 | : (event.path || []); 200 | 201 | // FIXME(web-padawan): `Symbol.iterator` used by webcomponentsjs is broken for arrays 202 | // example to check: `for...of` loop here throws the "Not yet implemented" error 203 | for (let i = 0; i < path.length; i++) { 204 | const target = path[i]; 205 | if (target.nodeName && target.nodeName.toLowerCase() === 'a') { 206 | anchor = target; 207 | break; 208 | } 209 | } 210 | 211 | while (anchor && anchor.nodeName.toLowerCase() !== 'a') { 212 | anchor = anchor.parentNode; 213 | } 214 | 215 | // ignore the click if not at an element 216 | if (!anchor || anchor.nodeName.toLowerCase() !== 'a') { 217 | return; 218 | } 219 | 220 | // ignore the click if the element has a non-default target 221 | if (anchor.target && anchor.target.toLowerCase() !== '_self') { 222 | return; 223 | } 224 | 225 | // ignore the click if the element has the 'download' attribute 226 | if (anchor.hasAttribute('download')) { 227 | return; 228 | } 229 | 230 | // ignore the click if the target URL is a fragment on the current page 231 | if (anchor.pathname === window.location.pathname && anchor.hash !== '') { 232 | return; 233 | } 234 | 235 | // ignore the click if the target is external to the app 236 | // In IE11 HTMLAnchorElement does not have the `origin` property 237 | const origin = anchor.origin || getAnchorOrigin(anchor); 238 | if (origin !== window.location.origin) { 239 | return; 240 | } 241 | 242 | // if none of the above, convert the click into a navigation event 243 | const {pathname, search, hash} = anchor; 244 | if (fireRouterEvent('go', {pathname, search, hash})) { 245 | event.preventDefault(); 246 | } 247 | } 248 | 249 | /** 250 | * A navigation trigger for Vaadin Router that translated clicks on `` links 251 | * into Vaadin Router navigation events. 252 | * 253 | * Only regular clicks on in-app links are translated (primary mouse button, no 254 | * modifier keys, the target href is within the app's URL space). 255 | * 256 | * @memberOf Router.Triggers 257 | * @type {NavigationTrigger} 258 | */ 259 | const CLICK = { 260 | activate() { 261 | window.document.addEventListener('click', vaadinRouterGlobalClickHandler); 262 | }, 263 | 264 | inactivate() { 265 | window.document.removeEventListener('click', vaadinRouterGlobalClickHandler); 266 | } 267 | }; 268 | 269 | // PopStateEvent constructor shim 270 | const isIE = /Trident/.test(navigator.userAgent); 271 | 272 | /* istanbul ignore next: coverage is calculated in Chrome, this code is for IE */ 273 | if (isIE && !isFunction(window.PopStateEvent)) { 274 | window.PopStateEvent = function(inType, params) { 275 | params = params || {}; 276 | var e = document.createEvent('Event'); 277 | e.initEvent(inType, Boolean(params.bubbles), Boolean(params.cancelable)); 278 | e.state = params.state || null; 279 | return e; 280 | }; 281 | window.PopStateEvent.prototype = window.Event.prototype; 282 | } 283 | 284 | function vaadinRouterGlobalPopstateHandler(event) { 285 | if (event.state === 'vaadin-router-ignore') { 286 | return; 287 | } 288 | const {pathname, search, hash} = window.location; 289 | fireRouterEvent('go', {pathname, search, hash}); 290 | } 291 | 292 | /** 293 | * A navigation trigger for Vaadin Router that translates popstate events into 294 | * Vaadin Router navigation events. 295 | * 296 | * @memberOf Router.Triggers 297 | * @type {NavigationTrigger} 298 | */ 299 | const POPSTATE = { 300 | activate() { 301 | window.addEventListener('popstate', vaadinRouterGlobalPopstateHandler); 302 | }, 303 | 304 | inactivate() { 305 | window.removeEventListener('popstate', vaadinRouterGlobalPopstateHandler); 306 | } 307 | }; 308 | 309 | /** 310 | * Expose `pathToRegexp`. 311 | */ 312 | var pathToRegexp_1 = pathToRegexp; 313 | var parse_1 = parse; 314 | var compile_1 = compile; 315 | var tokensToFunction_1 = tokensToFunction; 316 | var tokensToRegExp_1 = tokensToRegExp; 317 | 318 | /** 319 | * Default configs. 320 | */ 321 | var DEFAULT_DELIMITER = '/'; 322 | var DEFAULT_DELIMITERS = './'; 323 | 324 | /** 325 | * The main path matching regexp utility. 326 | * 327 | * @type {RegExp} 328 | */ 329 | var PATH_REGEXP = new RegExp([ 330 | // Match escaped characters that would otherwise appear in future matches. 331 | // This allows the user to escape special characters that won't transform. 332 | '(\\\\.)', 333 | // Match Express-style parameters and un-named parameters with a prefix 334 | // and optional suffixes. Matches appear as: 335 | // 336 | // ":test(\\d+)?" => ["test", "\d+", undefined, "?"] 337 | // "(\\d+)" => [undefined, undefined, "\d+", undefined] 338 | '(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?' 339 | ].join('|'), 'g'); 340 | 341 | /** 342 | * Parse a string for the raw tokens. 343 | * 344 | * @param {string} str 345 | * @param {Object=} options 346 | * @return {!Array} 347 | */ 348 | function parse (str, options) { 349 | var tokens = []; 350 | var key = 0; 351 | var index = 0; 352 | var path = ''; 353 | var defaultDelimiter = (options && options.delimiter) || DEFAULT_DELIMITER; 354 | var delimiters = (options && options.delimiters) || DEFAULT_DELIMITERS; 355 | var pathEscaped = false; 356 | var res; 357 | 358 | while ((res = PATH_REGEXP.exec(str)) !== null) { 359 | var m = res[0]; 360 | var escaped = res[1]; 361 | var offset = res.index; 362 | path += str.slice(index, offset); 363 | index = offset + m.length; 364 | 365 | // Ignore already escaped sequences. 366 | if (escaped) { 367 | path += escaped[1]; 368 | pathEscaped = true; 369 | continue 370 | } 371 | 372 | var prev = ''; 373 | var next = str[index]; 374 | var name = res[2]; 375 | var capture = res[3]; 376 | var group = res[4]; 377 | var modifier = res[5]; 378 | 379 | if (!pathEscaped && path.length) { 380 | var k = path.length - 1; 381 | 382 | if (delimiters.indexOf(path[k]) > -1) { 383 | prev = path[k]; 384 | path = path.slice(0, k); 385 | } 386 | } 387 | 388 | // Push the current path onto the tokens. 389 | if (path) { 390 | tokens.push(path); 391 | path = ''; 392 | pathEscaped = false; 393 | } 394 | 395 | var partial = prev !== '' && next !== undefined && next !== prev; 396 | var repeat = modifier === '+' || modifier === '*'; 397 | var optional = modifier === '?' || modifier === '*'; 398 | var delimiter = prev || defaultDelimiter; 399 | var pattern = capture || group; 400 | 401 | tokens.push({ 402 | name: name || key++, 403 | prefix: prev, 404 | delimiter: delimiter, 405 | optional: optional, 406 | repeat: repeat, 407 | partial: partial, 408 | pattern: pattern ? escapeGroup(pattern) : '[^' + escapeString(delimiter) + ']+?' 409 | }); 410 | } 411 | 412 | // Push any remaining characters. 413 | if (path || index < str.length) { 414 | tokens.push(path + str.substr(index)); 415 | } 416 | 417 | return tokens 418 | } 419 | 420 | /** 421 | * Compile a string to a template function for the path. 422 | * 423 | * @param {string} str 424 | * @param {Object=} options 425 | * @return {!function(Object=, Object=)} 426 | */ 427 | function compile (str, options) { 428 | return tokensToFunction(parse(str, options)) 429 | } 430 | 431 | /** 432 | * Expose a method for transforming tokens into the path function. 433 | */ 434 | function tokensToFunction (tokens) { 435 | // Compile all the tokens into regexps. 436 | var matches = new Array(tokens.length); 437 | 438 | // Compile all the patterns before compilation. 439 | for (var i = 0; i < tokens.length; i++) { 440 | if (typeof tokens[i] === 'object') { 441 | matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$'); 442 | } 443 | } 444 | 445 | return function (data, options) { 446 | var path = ''; 447 | var encode = (options && options.encode) || encodeURIComponent; 448 | 449 | for (var i = 0; i < tokens.length; i++) { 450 | var token = tokens[i]; 451 | 452 | if (typeof token === 'string') { 453 | path += token; 454 | continue 455 | } 456 | 457 | var value = data ? data[token.name] : undefined; 458 | var segment; 459 | 460 | if (Array.isArray(value)) { 461 | if (!token.repeat) { 462 | throw new TypeError('Expected "' + token.name + '" to not repeat, but got array') 463 | } 464 | 465 | if (value.length === 0) { 466 | if (token.optional) continue 467 | 468 | throw new TypeError('Expected "' + token.name + '" to not be empty') 469 | } 470 | 471 | for (var j = 0; j < value.length; j++) { 472 | segment = encode(value[j], token); 473 | 474 | if (!matches[i].test(segment)) { 475 | throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '"') 476 | } 477 | 478 | path += (j === 0 ? token.prefix : token.delimiter) + segment; 479 | } 480 | 481 | continue 482 | } 483 | 484 | if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { 485 | segment = encode(String(value), token); 486 | 487 | if (!matches[i].test(segment)) { 488 | throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but got "' + segment + '"') 489 | } 490 | 491 | path += token.prefix + segment; 492 | continue 493 | } 494 | 495 | if (token.optional) { 496 | // Prepend partial segment prefixes. 497 | if (token.partial) path += token.prefix; 498 | 499 | continue 500 | } 501 | 502 | throw new TypeError('Expected "' + token.name + '" to be ' + (token.repeat ? 'an array' : 'a string')) 503 | } 504 | 505 | return path 506 | } 507 | } 508 | 509 | /** 510 | * Escape a regular expression string. 511 | * 512 | * @param {string} str 513 | * @return {string} 514 | */ 515 | function escapeString (str) { 516 | return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1') 517 | } 518 | 519 | /** 520 | * Escape the capturing group by escaping special characters and meaning. 521 | * 522 | * @param {string} group 523 | * @return {string} 524 | */ 525 | function escapeGroup (group) { 526 | return group.replace(/([=!:$/()])/g, '\\$1') 527 | } 528 | 529 | /** 530 | * Get the flags for a regexp from the options. 531 | * 532 | * @param {Object} options 533 | * @return {string} 534 | */ 535 | function flags (options) { 536 | return options && options.sensitive ? '' : 'i' 537 | } 538 | 539 | /** 540 | * Pull out keys from a regexp. 541 | * 542 | * @param {!RegExp} path 543 | * @param {Array=} keys 544 | * @return {!RegExp} 545 | */ 546 | function regexpToRegexp (path, keys) { 547 | if (!keys) return path 548 | 549 | // Use a negative lookahead to match only capturing groups. 550 | var groups = path.source.match(/\((?!\?)/g); 551 | 552 | if (groups) { 553 | for (var i = 0; i < groups.length; i++) { 554 | keys.push({ 555 | name: i, 556 | prefix: null, 557 | delimiter: null, 558 | optional: false, 559 | repeat: false, 560 | partial: false, 561 | pattern: null 562 | }); 563 | } 564 | } 565 | 566 | return path 567 | } 568 | 569 | /** 570 | * Transform an array into a regexp. 571 | * 572 | * @param {!Array} path 573 | * @param {Array=} keys 574 | * @param {Object=} options 575 | * @return {!RegExp} 576 | */ 577 | function arrayToRegexp (path, keys, options) { 578 | var parts = []; 579 | 580 | for (var i = 0; i < path.length; i++) { 581 | parts.push(pathToRegexp(path[i], keys, options).source); 582 | } 583 | 584 | return new RegExp('(?:' + parts.join('|') + ')', flags(options)) 585 | } 586 | 587 | /** 588 | * Create a path regexp from string input. 589 | * 590 | * @param {string} path 591 | * @param {Array=} keys 592 | * @param {Object=} options 593 | * @return {!RegExp} 594 | */ 595 | function stringToRegexp (path, keys, options) { 596 | return tokensToRegExp(parse(path, options), keys, options) 597 | } 598 | 599 | /** 600 | * Expose a function for taking tokens and returning a RegExp. 601 | * 602 | * @param {!Array} tokens 603 | * @param {Array=} keys 604 | * @param {Object=} options 605 | * @return {!RegExp} 606 | */ 607 | function tokensToRegExp (tokens, keys, options) { 608 | options = options || {}; 609 | 610 | var strict = options.strict; 611 | var start = options.start !== false; 612 | var end = options.end !== false; 613 | var delimiter = escapeString(options.delimiter || DEFAULT_DELIMITER); 614 | var delimiters = options.delimiters || DEFAULT_DELIMITERS; 615 | var endsWith = [].concat(options.endsWith || []).map(escapeString).concat('$').join('|'); 616 | var route = start ? '^' : ''; 617 | var isEndDelimited = tokens.length === 0; 618 | 619 | // Iterate over the tokens and create our regexp string. 620 | for (var i = 0; i < tokens.length; i++) { 621 | var token = tokens[i]; 622 | 623 | if (typeof token === 'string') { 624 | route += escapeString(token); 625 | isEndDelimited = i === tokens.length - 1 && delimiters.indexOf(token[token.length - 1]) > -1; 626 | } else { 627 | var capture = token.repeat 628 | ? '(?:' + token.pattern + ')(?:' + escapeString(token.delimiter) + '(?:' + token.pattern + '))*' 629 | : token.pattern; 630 | 631 | if (keys) keys.push(token); 632 | 633 | if (token.optional) { 634 | if (token.partial) { 635 | route += escapeString(token.prefix) + '(' + capture + ')?'; 636 | } else { 637 | route += '(?:' + escapeString(token.prefix) + '(' + capture + '))?'; 638 | } 639 | } else { 640 | route += escapeString(token.prefix) + '(' + capture + ')'; 641 | } 642 | } 643 | } 644 | 645 | if (end) { 646 | if (!strict) route += '(?:' + delimiter + ')?'; 647 | 648 | route += endsWith === '$' ? '$' : '(?=' + endsWith + ')'; 649 | } else { 650 | if (!strict) route += '(?:' + delimiter + '(?=' + endsWith + '))?'; 651 | if (!isEndDelimited) route += '(?=' + delimiter + '|' + endsWith + ')'; 652 | } 653 | 654 | return new RegExp(route, flags(options)) 655 | } 656 | 657 | /** 658 | * Normalize the given path string, returning a regular expression. 659 | * 660 | * An empty array can be passed in for the keys, which will hold the 661 | * placeholder key descriptions. For example, using `/user/:id`, `keys` will 662 | * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. 663 | * 664 | * @param {(string|RegExp|Array)} path 665 | * @param {Array=} keys 666 | * @param {Object=} options 667 | * @return {!RegExp} 668 | */ 669 | function pathToRegexp (path, keys, options) { 670 | if (path instanceof RegExp) { 671 | return regexpToRegexp(path, keys) 672 | } 673 | 674 | if (Array.isArray(path)) { 675 | return arrayToRegexp(/** @type {!Array} */ (path), keys, options) 676 | } 677 | 678 | return stringToRegexp(/** @type {string} */ (path), keys, options) 679 | } 680 | pathToRegexp_1.parse = parse_1; 681 | pathToRegexp_1.compile = compile_1; 682 | pathToRegexp_1.tokensToFunction = tokensToFunction_1; 683 | pathToRegexp_1.tokensToRegExp = tokensToRegExp_1; 684 | 685 | /** 686 | * Universal Router (https://www.kriasoft.com/universal-router/) 687 | * 688 | * Copyright (c) 2015-present Kriasoft. 689 | * 690 | * This source code is licensed under the MIT license found in the 691 | * LICENSE.txt file in the root directory of this source tree. 692 | */ 693 | 694 | const {hasOwnProperty} = Object.prototype; 695 | const cache = new Map(); 696 | // see https://github.com/pillarjs/path-to-regexp/issues/148 697 | cache.set('|false', { 698 | keys: [], 699 | pattern: /(?:)/ 700 | }); 701 | 702 | function decodeParam(val) { 703 | try { 704 | return decodeURIComponent(val); 705 | } catch (err) { 706 | return val; 707 | } 708 | } 709 | 710 | function matchPath(routepath, path, exact, parentKeys, parentParams) { 711 | exact = !!exact; 712 | const cacheKey = `${routepath}|${exact}`; 713 | let regexp = cache.get(cacheKey); 714 | 715 | if (!regexp) { 716 | const keys = []; 717 | regexp = { 718 | keys, 719 | pattern: pathToRegexp_1(routepath, keys, { 720 | end: exact, 721 | strict: routepath === '' 722 | }), 723 | }; 724 | cache.set(cacheKey, regexp); 725 | } 726 | 727 | const m = regexp.pattern.exec(path); 728 | if (!m) { 729 | return null; 730 | } 731 | 732 | const params = Object.assign({}, parentParams); 733 | 734 | for (let i = 1; i < m.length; i++) { 735 | const key = regexp.keys[i - 1]; 736 | const prop = key.name; 737 | const value = m[i]; 738 | if (value !== undefined || !hasOwnProperty.call(params, prop)) { 739 | if (key.repeat) { 740 | params[prop] = value ? value.split(key.delimiter).map(decodeParam) : []; 741 | } else { 742 | params[prop] = value ? decodeParam(value) : value; 743 | } 744 | } 745 | } 746 | 747 | return { 748 | path: m[0], 749 | keys: (parentKeys || []).concat(regexp.keys), 750 | params, 751 | }; 752 | } 753 | 754 | /** 755 | * Universal Router (https://www.kriasoft.com/universal-router/) 756 | * 757 | * Copyright (c) 2015-present Kriasoft. 758 | * 759 | * This source code is licensed under the MIT license found in the 760 | * LICENSE.txt file in the root directory of this source tree. 761 | */ 762 | 763 | /** 764 | * Traverses the routes tree and matches its nodes to the given pathname from 765 | * the root down to the leaves. Each match consumes a part of the pathname and 766 | * the matching process continues for as long as there is a matching child 767 | * route for the remaining part of the pathname. 768 | * 769 | * The returned value is a lazily evaluated iterator. 770 | * 771 | * The leading "/" in a route path matters only for the root of the routes 772 | * tree (or if all parent routes are ""). In all other cases a leading "/" in 773 | * a child route path has no significance. 774 | * 775 | * The trailing "/" in a _route path_ matters only for the leaves of the 776 | * routes tree. A leaf route with a trailing "/" matches only a pathname that 777 | * also has a trailing "/". 778 | * 779 | * The trailing "/" in a route path does not affect matching of child routes 780 | * in any way. 781 | * 782 | * The trailing "/" in a _pathname_ generally does not matter (except for 783 | * the case of leaf nodes described above). 784 | * 785 | * The "" and "/" routes have special treatment: 786 | * 1. as a single route 787 | * the "" and "/" routes match only the "" and "/" pathnames respectively 788 | * 2. as a parent in the routes tree 789 | * the "" route matches any pathname without consuming any part of it 790 | * the "/" route matches any absolute pathname consuming its leading "/" 791 | * 3. as a leaf in the routes tree 792 | * the "" and "/" routes match only if the entire pathname is consumed by 793 | * the parent routes chain. In this case "" and "/" are equivalent. 794 | * 4. several directly nested "" or "/" routes 795 | * - directly nested "" or "/" routes are 'squashed' (i.e. nesting two 796 | * "/" routes does not require a double "/" in the pathname to match) 797 | * - if there are only "" in the parent routes chain, no part of the 798 | * pathname is consumed, and the leading "/" in the child routes' paths 799 | * remains significant 800 | * 801 | * Side effect: 802 | * - the routes tree { path: '' } matches only the '' pathname 803 | * - the routes tree { path: '', children: [ { path: '' } ] } matches any 804 | * pathname (for the tree root) 805 | * 806 | * Prefix matching can be enabled also by `children: true`. 807 | */ 808 | function matchRoute(route, pathname, ignoreLeadingSlash, parentKeys, parentParams) { 809 | let match; 810 | let childMatches; 811 | let childIndex = 0; 812 | let routepath = route.path || ''; 813 | if (routepath.charAt(0) === '/') { 814 | if (ignoreLeadingSlash) { 815 | routepath = routepath.substr(1); 816 | } 817 | ignoreLeadingSlash = true; 818 | } 819 | 820 | return { 821 | next(routeToSkip) { 822 | if (route === routeToSkip) { 823 | return {done: true}; 824 | } 825 | 826 | const children = route.__children = route.__children || route.children; 827 | 828 | if (!match) { 829 | match = matchPath(routepath, pathname, !children, parentKeys, parentParams); 830 | 831 | if (match) { 832 | return { 833 | done: false, 834 | value: { 835 | route, 836 | keys: match.keys, 837 | params: match.params, 838 | path: match.path 839 | }, 840 | }; 841 | } 842 | } 843 | 844 | if (match && children) { 845 | while (childIndex < children.length) { 846 | if (!childMatches) { 847 | const childRoute = children[childIndex]; 848 | childRoute.parent = route; 849 | 850 | let matchedLength = match.path.length; 851 | if (matchedLength > 0 && pathname.charAt(matchedLength) === '/') { 852 | matchedLength += 1; 853 | } 854 | 855 | childMatches = matchRoute( 856 | childRoute, 857 | pathname.substr(matchedLength), 858 | ignoreLeadingSlash, 859 | match.keys, 860 | match.params 861 | ); 862 | } 863 | 864 | const childMatch = childMatches.next(routeToSkip); 865 | if (!childMatch.done) { 866 | return { 867 | done: false, 868 | value: childMatch.value, 869 | }; 870 | } 871 | 872 | childMatches = null; 873 | childIndex++; 874 | } 875 | } 876 | 877 | return {done: true}; 878 | }, 879 | }; 880 | } 881 | 882 | /** 883 | * Universal Router (https://www.kriasoft.com/universal-router/) 884 | * 885 | * Copyright (c) 2015-present Kriasoft. 886 | * 887 | * This source code is licensed under the MIT license found in the 888 | * LICENSE.txt file in the root directory of this source tree. 889 | */ 890 | 891 | function resolveRoute(context) { 892 | if (isFunction(context.route.action)) { 893 | return context.route.action(context); 894 | } 895 | return undefined; 896 | } 897 | 898 | /** 899 | * Universal Router (https://www.kriasoft.com/universal-router/) 900 | * 901 | * Copyright (c) 2015-present Kriasoft. 902 | * 903 | * This source code is licensed under the MIT license found in the 904 | * LICENSE.txt file in the root directory of this source tree. 905 | */ 906 | 907 | function isChildRoute(parentRoute, childRoute) { 908 | let route = childRoute; 909 | while (route) { 910 | route = route.parent; 911 | if (route === parentRoute) { 912 | return true; 913 | } 914 | } 915 | return false; 916 | } 917 | 918 | function generateErrorMessage(currentContext) { 919 | let errorMessage = `Path '${currentContext.pathname}' is not properly resolved due to an error.`; 920 | const routePath = (currentContext.route || {}).path; 921 | if (routePath) { 922 | errorMessage += ` Resolution had failed on route: '${routePath}'`; 923 | } 924 | return errorMessage; 925 | } 926 | 927 | function addRouteToChain(context, match) { 928 | const {route, path} = match; 929 | function shouldDiscardOldChain(oldChain, route) { 930 | return !route.parent || !oldChain || !oldChain.length || oldChain[oldChain.length - 1].route !== route.parent; 931 | } 932 | 933 | if (route && !route.__synthetic) { 934 | const item = {path, route}; 935 | if (shouldDiscardOldChain(context.chain, route)) { 936 | context.chain = [item]; 937 | } else { 938 | context.chain.push(item); 939 | } 940 | } 941 | } 942 | 943 | /** 944 | */ 945 | class Resolver { 946 | constructor(routes, options = {}) { 947 | if (Object(routes) !== routes) { 948 | throw new TypeError('Invalid routes'); 949 | } 950 | 951 | this.baseUrl = options.baseUrl || ''; 952 | this.errorHandler = options.errorHandler; 953 | this.resolveRoute = options.resolveRoute || resolveRoute; 954 | this.context = Object.assign({resolver: this}, options.context); 955 | this.root = Array.isArray(routes) ? {path: '', __children: routes, parent: null, __synthetic: true} : routes; 956 | this.root.parent = null; 957 | } 958 | 959 | /** 960 | * Returns the current list of routes (as a shallow copy). Adding / removing 961 | * routes to / from the returned array does not affect the routing config, 962 | * but modifying the route objects does. 963 | * 964 | * @return {!Array} 965 | */ 966 | getRoutes() { 967 | return [...this.root.__children]; 968 | } 969 | 970 | /** 971 | * Sets the routing config (replacing the existing one). 972 | * 973 | * @param {!Array|!Router.Route} routes a single route or an array of those 974 | * (the array is shallow copied) 975 | */ 976 | setRoutes(routes) { 977 | ensureRoutes(routes); 978 | const newRoutes = [...toArray(routes)]; 979 | this.root.__children = newRoutes; 980 | } 981 | 982 | /** 983 | * Appends one or several routes to the routing config and returns the 984 | * effective routing config after the operation. 985 | * 986 | * @param {!Array|!Router.Route} routes a single route or an array of those 987 | * (the array is shallow copied) 988 | * @return {!Array} 989 | * @protected 990 | */ 991 | addRoutes(routes) { 992 | ensureRoutes(routes); 993 | this.root.__children.push(...toArray(routes)); 994 | return this.getRoutes(); 995 | } 996 | 997 | /** 998 | * Removes all existing routes from the routing config. 999 | */ 1000 | removeRoutes() { 1001 | this.setRoutes([]); 1002 | } 1003 | 1004 | /** 1005 | * Asynchronously resolves the given pathname, i.e. finds all routes matching 1006 | * the pathname and tries resolving them one after another in the order they 1007 | * are listed in the routes config until the first non-null result. 1008 | * 1009 | * Returns a promise that is fulfilled with the return value of an object that consists of the first 1010 | * route handler result that returns something other than `null` or `undefined` and context used to get this result. 1011 | * 1012 | * If no route handlers return a non-null result, or if no route matches the 1013 | * given pathname the returned promise is rejected with a 'page not found' 1014 | * `Error`. 1015 | * 1016 | * @param {!string|!{pathname: !string}} pathnameOrContext the pathname to 1017 | * resolve or a context object with a `pathname` property and other 1018 | * properties to pass to the route resolver functions. 1019 | * @return {!Promise} 1020 | */ 1021 | resolve(pathnameOrContext) { 1022 | const context = Object.assign( 1023 | {}, 1024 | this.context, 1025 | isString(pathnameOrContext) ? {pathname: pathnameOrContext} : pathnameOrContext 1026 | ); 1027 | const match = matchRoute( 1028 | this.root, 1029 | this.__normalizePathname(context.pathname), 1030 | this.baseUrl 1031 | ); 1032 | const resolve = this.resolveRoute; 1033 | let matches = null; 1034 | let nextMatches = null; 1035 | let currentContext = context; 1036 | 1037 | function next(resume, parent = matches.value.route, prevResult) { 1038 | const routeToSkip = prevResult === null && matches.value.route; 1039 | matches = nextMatches || match.next(routeToSkip); 1040 | nextMatches = null; 1041 | 1042 | if (!resume) { 1043 | if (matches.done || !isChildRoute(parent, matches.value.route)) { 1044 | nextMatches = matches; 1045 | return Promise.resolve(notFoundResult); 1046 | } 1047 | } 1048 | 1049 | if (matches.done) { 1050 | return Promise.reject(getNotFoundError(context)); 1051 | } 1052 | 1053 | addRouteToChain(context, matches.value); 1054 | currentContext = Object.assign({}, context, matches.value); 1055 | 1056 | return Promise.resolve(resolve(currentContext)).then(resolution => { 1057 | if (resolution !== null && resolution !== undefined && resolution !== notFoundResult) { 1058 | currentContext.result = resolution.result || resolution; 1059 | return currentContext; 1060 | } 1061 | return next(resume, parent, resolution); 1062 | }); 1063 | } 1064 | 1065 | context.next = next; 1066 | 1067 | return Promise.resolve() 1068 | .then(() => next(true, this.root)) 1069 | .catch((error) => { 1070 | const errorMessage = generateErrorMessage(currentContext); 1071 | if (!error) { 1072 | error = new Error(errorMessage); 1073 | } else { 1074 | console.warn(errorMessage); 1075 | } 1076 | error.context = error.context || currentContext; 1077 | // DOMException has its own code which is read-only 1078 | if (!(error instanceof DOMException)) { 1079 | error.code = error.code || 500; 1080 | } 1081 | if (this.errorHandler) { 1082 | currentContext.result = this.errorHandler(error); 1083 | return currentContext; 1084 | } 1085 | throw error; 1086 | }); 1087 | } 1088 | 1089 | /** 1090 | * URL constructor polyfill hook. Creates and returns an URL instance. 1091 | */ 1092 | static __createUrl(url, base) { 1093 | return new URL(url, base); 1094 | } 1095 | 1096 | /** 1097 | * If the baseUrl property is set, transforms the baseUrl and returns the full 1098 | * actual `base` string for using in the `new URL(path, base);` and for 1099 | * prepernding the paths with. The returned base ends with a trailing slash. 1100 | * 1101 | * Otherwise, returns empty string. 1102 | */ 1103 | get __effectiveBaseUrl() { 1104 | return this.baseUrl 1105 | ? this.constructor.__createUrl( 1106 | this.baseUrl, 1107 | document.baseURI || document.URL 1108 | ).href.replace(/[^\/]*$/, '') 1109 | : ''; 1110 | } 1111 | 1112 | /** 1113 | * If the baseUrl is set, matches the pathname with the router’s baseUrl, 1114 | * and returns the local pathname with the baseUrl stripped out. 1115 | * 1116 | * If the pathname does not match the baseUrl, returns undefined. 1117 | * 1118 | * If the `baseUrl` is not set, returns the unmodified pathname argument. 1119 | */ 1120 | __normalizePathname(pathname) { 1121 | if (!this.baseUrl) { 1122 | // No base URL, no need to transform the pathname. 1123 | return pathname; 1124 | } 1125 | 1126 | const base = this.__effectiveBaseUrl; 1127 | const normalizedUrl = this.constructor.__createUrl(pathname, base).href; 1128 | if (normalizedUrl.slice(0, base.length) === base) { 1129 | return normalizedUrl.slice(base.length); 1130 | } 1131 | } 1132 | } 1133 | 1134 | Resolver.pathToRegexp = pathToRegexp_1; 1135 | 1136 | /** 1137 | * Universal Router (https://www.kriasoft.com/universal-router/) 1138 | * 1139 | * Copyright (c) 2015-present Kriasoft. 1140 | * 1141 | * This source code is licensed under the MIT license found in the 1142 | * LICENSE.txt file in the root directory of this source tree. 1143 | */ 1144 | 1145 | const {pathToRegexp: pathToRegexp$1} = Resolver; 1146 | const cache$1 = new Map(); 1147 | 1148 | function cacheRoutes(routesByName, route, routes) { 1149 | const name = route.name || route.component; 1150 | if (name) { 1151 | if (routesByName.has(name)) { 1152 | routesByName.get(name).push(route); 1153 | } else { 1154 | routesByName.set(name, [route]); 1155 | } 1156 | } 1157 | 1158 | if (Array.isArray(routes)) { 1159 | for (let i = 0; i < routes.length; i++) { 1160 | const childRoute = routes[i]; 1161 | childRoute.parent = route; 1162 | cacheRoutes(routesByName, childRoute, childRoute.__children || childRoute.children); 1163 | } 1164 | } 1165 | } 1166 | 1167 | function getRouteByName(routesByName, routeName) { 1168 | const routes = routesByName.get(routeName); 1169 | if (routes && routes.length > 1) { 1170 | throw new Error( 1171 | `Duplicate route with name "${routeName}".` 1172 | + ` Try seting unique 'name' route properties.` 1173 | ); 1174 | } 1175 | return routes && routes[0]; 1176 | } 1177 | 1178 | function getRoutePath(route) { 1179 | let path = route.path; 1180 | path = Array.isArray(path) ? path[0] : path; 1181 | return path !== undefined ? path : ''; 1182 | } 1183 | 1184 | function generateUrls(router, options = {}) { 1185 | if (!(router instanceof Resolver)) { 1186 | throw new TypeError('An instance of Resolver is expected'); 1187 | } 1188 | 1189 | const routesByName = new Map(); 1190 | 1191 | return (routeName, params) => { 1192 | let route = getRouteByName(routesByName, routeName); 1193 | if (!route) { 1194 | routesByName.clear(); // clear cache 1195 | cacheRoutes(routesByName, router.root, router.root.__children); 1196 | 1197 | route = getRouteByName(routesByName, routeName); 1198 | if (!route) { 1199 | throw new Error(`Route "${routeName}" not found`); 1200 | } 1201 | } 1202 | 1203 | let regexp = cache$1.get(route.fullPath); 1204 | if (!regexp) { 1205 | let fullPath = getRoutePath(route); 1206 | let rt = route.parent; 1207 | while (rt) { 1208 | const path = getRoutePath(rt); 1209 | if (path) { 1210 | fullPath = path.replace(/\/$/, '') + '/' + fullPath.replace(/^\//, ''); 1211 | } 1212 | rt = rt.parent; 1213 | } 1214 | const tokens = pathToRegexp$1.parse(fullPath); 1215 | const toPath = pathToRegexp$1.tokensToFunction(tokens); 1216 | const keys = Object.create(null); 1217 | for (let i = 0; i < tokens.length; i++) { 1218 | if (!isString(tokens[i])) { 1219 | keys[tokens[i].name] = true; 1220 | } 1221 | } 1222 | regexp = {toPath, keys}; 1223 | cache$1.set(fullPath, regexp); 1224 | route.fullPath = fullPath; 1225 | } 1226 | 1227 | let url = regexp.toPath(params, options) || '/'; 1228 | 1229 | if (options.stringifyQueryParams && params) { 1230 | const queryParams = {}; 1231 | const keys = Object.keys(params); 1232 | for (let i = 0; i < keys.length; i++) { 1233 | const key = keys[i]; 1234 | if (!regexp.keys[key]) { 1235 | queryParams[key] = params[key]; 1236 | } 1237 | } 1238 | const query = options.stringifyQueryParams(queryParams); 1239 | if (query) { 1240 | url += query.charAt(0) === '?' ? query : `?${query}`; 1241 | } 1242 | } 1243 | 1244 | return url; 1245 | }; 1246 | } 1247 | 1248 | /** 1249 | * @typedef NavigationTrigger 1250 | * @type {object} 1251 | * @property {function()} activate 1252 | * @property {function()} inactivate 1253 | */ 1254 | 1255 | /** @type {Array} */ 1256 | let triggers = []; 1257 | 1258 | function setNavigationTriggers(newTriggers) { 1259 | triggers.forEach(trigger => trigger.inactivate()); 1260 | 1261 | newTriggers.forEach(trigger => trigger.activate()); 1262 | 1263 | triggers = newTriggers; 1264 | } 1265 | 1266 | const willAnimate = elem => { 1267 | const name = getComputedStyle(elem).getPropertyValue('animation-name'); 1268 | return name && name !== 'none'; 1269 | }; 1270 | 1271 | const waitForAnimation = (elem, cb) => { 1272 | const listener = () => { 1273 | elem.removeEventListener('animationend', listener); 1274 | cb(); 1275 | }; 1276 | elem.addEventListener('animationend', listener); 1277 | }; 1278 | 1279 | function animate(elem, className) { 1280 | elem.classList.add(className); 1281 | 1282 | return new Promise(resolve => { 1283 | if (willAnimate(elem)) { 1284 | const rect = elem.getBoundingClientRect(); 1285 | const size = `height: ${rect.bottom - rect.top}px; width: ${rect.right - rect.left}px`; 1286 | elem.setAttribute('style', `position: absolute; ${size}`); 1287 | waitForAnimation(elem, () => { 1288 | elem.classList.remove(className); 1289 | elem.removeAttribute('style'); 1290 | resolve(); 1291 | }); 1292 | } else { 1293 | elem.classList.remove(className); 1294 | resolve(); 1295 | } 1296 | }); 1297 | } 1298 | 1299 | const MAX_REDIRECT_COUNT = 256; 1300 | 1301 | function isResultNotEmpty(result) { 1302 | return result !== null && result !== undefined; 1303 | } 1304 | 1305 | function copyContextWithoutNext(context) { 1306 | const copy = Object.assign({}, context); 1307 | delete copy.next; 1308 | return copy; 1309 | } 1310 | 1311 | function createLocation({pathname = '', search = '', hash = '', chain = [], params = {}, redirectFrom, resolver}, route) { 1312 | const routes = chain.map(item => item.route); 1313 | return { 1314 | baseUrl: resolver && resolver.baseUrl || '', 1315 | pathname, 1316 | search, 1317 | hash, 1318 | routes, 1319 | route: route || routes.length && routes[routes.length - 1] || null, 1320 | params, 1321 | redirectFrom, 1322 | getUrl: (userParams = {}) => getPathnameForRouter( 1323 | Router.pathToRegexp.compile( 1324 | getMatchedPath(routes) 1325 | )(Object.assign({}, params, userParams)), 1326 | resolver 1327 | ) 1328 | }; 1329 | } 1330 | 1331 | function createRedirect(context, pathname) { 1332 | const params = Object.assign({}, context.params); 1333 | return { 1334 | redirect: { 1335 | pathname, 1336 | from: context.pathname, 1337 | params 1338 | } 1339 | }; 1340 | } 1341 | 1342 | function renderElement(context, element) { 1343 | element.location = createLocation(context); 1344 | const index = context.chain.map(item => item.route).indexOf(context.route); 1345 | context.chain[index].element = element; 1346 | return element; 1347 | } 1348 | 1349 | function runCallbackIfPossible(callback, args, thisArg) { 1350 | if (isFunction(callback)) { 1351 | return callback.apply(thisArg, args); 1352 | } 1353 | } 1354 | 1355 | function amend(amendmentFunction, args, element) { 1356 | return amendmentResult => { 1357 | if (amendmentResult && (amendmentResult.cancel || amendmentResult.redirect)) { 1358 | return amendmentResult; 1359 | } 1360 | 1361 | if (element) { 1362 | return runCallbackIfPossible(element[amendmentFunction], args, element); 1363 | } 1364 | }; 1365 | } 1366 | 1367 | function processNewChildren(newChildren, route) { 1368 | if (!Array.isArray(newChildren) && !isObject(newChildren)) { 1369 | throw new Error( 1370 | log( 1371 | `Incorrect "children" value for the route ${route.path}: expected array or object, but got ${newChildren}` 1372 | ) 1373 | ); 1374 | } 1375 | 1376 | route.__children = []; 1377 | const childRoutes = toArray(newChildren); 1378 | for (let i = 0; i < childRoutes.length; i++) { 1379 | ensureRoute(childRoutes[i]); 1380 | route.__children.push(childRoutes[i]); 1381 | } 1382 | } 1383 | 1384 | function removeDomNodes(nodes) { 1385 | if (nodes && nodes.length) { 1386 | const parent = nodes[0].parentNode; 1387 | for (let i = 0; i < nodes.length; i++) { 1388 | parent.removeChild(nodes[i]); 1389 | } 1390 | } 1391 | } 1392 | 1393 | function getPathnameForRouter(pathname, router) { 1394 | const base = router.__effectiveBaseUrl; 1395 | return base 1396 | ? router.constructor.__createUrl(pathname.replace(/^\//, ''), base).pathname 1397 | : pathname; 1398 | } 1399 | 1400 | function getMatchedPath(chain) { 1401 | return chain.map(item => item.path).reduce((a, b) => { 1402 | if (b.length) { 1403 | return a.replace(/\/$/, '') + '/' + b.replace(/^\//, ''); 1404 | } 1405 | return a; 1406 | }, ''); 1407 | } 1408 | 1409 | /** 1410 | * A simple client-side router for single-page applications. It uses 1411 | * express-style middleware and has a first-class support for Web Components and 1412 | * lazy-loading. Works great in Polymer and non-Polymer apps. 1413 | * 1414 | * Use `new Router(outlet, options)` to create a new Router instance. 1415 | * 1416 | * * The `outlet` parameter is a reference to the DOM node to render 1417 | * the content into. 1418 | * 1419 | * * The `options` parameter is an optional object with options. The following 1420 | * keys are supported: 1421 | * * `baseUrl` — the initial value for [ 1422 | * the `baseUrl` property 1423 | * ](#/classes/Router#property-baseUrl) 1424 | * 1425 | * The Router instance is automatically subscribed to navigation events 1426 | * on `window`. 1427 | * 1428 | * See [Live Examples](#/classes/Router/demos/demo/index.html) for the detailed usage demo and code snippets. 1429 | * 1430 | * See also detailed API docs for the following methods, for the advanced usage: 1431 | * 1432 | * * [setOutlet](#/classes/Router#method-setOutlet) – should be used to configure the outlet. 1433 | * * [setTriggers](#/classes/Router#method-setTriggers) – should be used to configure the navigation events. 1434 | * * [setRoutes](#/classes/Router#method-setRoutes) – should be used to configure the routes. 1435 | * 1436 | * Only `setRoutes` has to be called manually, others are automatically invoked when creating a new instance. 1437 | * 1438 | * @extends Resolver 1439 | * @demo demo/index.html 1440 | * @summary JavaScript class that renders different DOM content depending on 1441 | * a given path. It can re-render when triggered or automatically on 1442 | * 'popstate' and / or 'click' events. 1443 | */ 1444 | class Router extends Resolver { 1445 | 1446 | /** 1447 | * Creates a new Router instance with a given outlet, and 1448 | * automatically subscribes it to navigation events on the `window`. 1449 | * Using a constructor argument or a setter for outlet is equivalent: 1450 | * 1451 | * ``` 1452 | * const router = new Router(); 1453 | * router.setOutlet(outlet); 1454 | * ``` 1455 | * @param {?Node=} outlet 1456 | * @param {?Router.Options=} options 1457 | */ 1458 | constructor(outlet, options) { 1459 | const baseElement = document.head.querySelector('base'); 1460 | super([], Object.assign({ 1461 | // Default options 1462 | baseUrl: baseElement && baseElement.getAttribute('href') 1463 | }, options)); 1464 | 1465 | this.resolveRoute = context => this.__resolveRoute(context); 1466 | 1467 | const triggers = Router.NavigationTrigger; 1468 | Router.setTriggers.apply(Router, Object.keys(triggers).map(key => triggers[key])); 1469 | 1470 | /** 1471 | * The base URL for all routes in the router instance. By default, 1472 | * takes the `` attribute value if the base element exists 1473 | * in the ``. 1474 | * 1475 | * @public 1476 | * @type {string} 1477 | */ 1478 | this.baseUrl; 1479 | 1480 | /** 1481 | * A promise that is settled after the current render cycle completes. If 1482 | * there is no render cycle in progress the promise is immediately settled 1483 | * with the last render cycle result. 1484 | * 1485 | * @public 1486 | * @type {!Promise} 1487 | */ 1488 | this.ready; 1489 | this.ready = Promise.resolve(outlet); 1490 | 1491 | /** 1492 | * Contains read-only information about the current router location: 1493 | * pathname, active routes, parameters. See the 1494 | * [Location type declaration](#/classes/Router.Location) 1495 | * for more details. 1496 | * 1497 | * @public 1498 | * @type {!Router.Location} 1499 | */ 1500 | this.location; 1501 | this.location = createLocation({resolver: this}); 1502 | 1503 | this.__lastStartedRenderId = 0; 1504 | this.__navigationEventHandler = this.__onNavigationEvent.bind(this); 1505 | this.setOutlet(outlet); 1506 | this.subscribe(); 1507 | // Using WeakMap instead of WeakSet because WeakSet is not supported by IE11 1508 | this.__createdByRouter = new WeakMap(); 1509 | this.__addedByRouter = new WeakMap(); 1510 | } 1511 | 1512 | __resolveRoute(context) { 1513 | const route = context.route; 1514 | 1515 | let callbacks = Promise.resolve(); 1516 | 1517 | if (isFunction(route.children)) { 1518 | callbacks = callbacks 1519 | .then(() => route.children(copyContextWithoutNext(context))) 1520 | .then(children => { 1521 | // The route.children() callback might have re-written the 1522 | // route.children property instead of returning a value 1523 | if (!isResultNotEmpty(children) && !isFunction(route.children)) { 1524 | children = route.children; 1525 | } 1526 | processNewChildren(children, route); 1527 | }); 1528 | } 1529 | 1530 | const commands = { 1531 | redirect: path => createRedirect(context, path), 1532 | component: (component) => { 1533 | const element = document.createElement(component); 1534 | this.__createdByRouter.set(element, true); 1535 | return element; 1536 | } 1537 | }; 1538 | 1539 | return callbacks 1540 | .then(() => { 1541 | if (this.__isLatestRender(context)) { 1542 | return runCallbackIfPossible(route.action, [context, commands], route); 1543 | } 1544 | }) 1545 | .then(result => { 1546 | if (isResultNotEmpty(result)) { 1547 | // Actions like `() => import('my-view.js')` are not expected to 1548 | // end the resolution, despite the result is not empty. Checking 1549 | // the result with a whitelist of values that end the resolution. 1550 | if (result instanceof HTMLElement || 1551 | result.redirect || 1552 | result === notFoundResult) { 1553 | return result; 1554 | } 1555 | } 1556 | 1557 | if (isString(route.redirect)) { 1558 | return commands.redirect(route.redirect); 1559 | } 1560 | 1561 | if (route.bundle) { 1562 | return loadBundle(route.bundle) 1563 | .then(() => {}, () => { 1564 | throw new Error(log(`Bundle not found: ${route.bundle}. Check if the file name is correct`)); 1565 | }); 1566 | } 1567 | }) 1568 | .then(result => { 1569 | if (isResultNotEmpty(result)) { 1570 | return result; 1571 | } 1572 | if (isString(route.component)) { 1573 | return commands.component(route.component); 1574 | } 1575 | }); 1576 | } 1577 | 1578 | /** 1579 | * Sets the router outlet (the DOM node where the content for the current 1580 | * route is inserted). Any content pre-existing in the router outlet is 1581 | * removed at the end of each render pass. 1582 | * 1583 | * NOTE: this method is automatically invoked first time when creating a new Router instance. 1584 | * 1585 | * @param {?Node} outlet the DOM node where the content for the current route 1586 | * is inserted. 1587 | */ 1588 | setOutlet(outlet) { 1589 | if (outlet) { 1590 | this.__ensureOutlet(outlet); 1591 | } 1592 | this.__outlet = outlet; 1593 | } 1594 | 1595 | /** 1596 | * Returns the current router outlet. The initial value is `undefined`. 1597 | * 1598 | * @return {?Node} the current router outlet (or `undefined`) 1599 | */ 1600 | getOutlet() { 1601 | return this.__outlet; 1602 | } 1603 | 1604 | /** 1605 | * Sets the routing config (replacing the existing one) and triggers a 1606 | * navigation event so that the router outlet is refreshed according to the 1607 | * current `window.location` and the new routing config. 1608 | * 1609 | * Each route object may have the following properties, listed here in the processing order: 1610 | * * `path` – the route path (relative to the parent route if any) in the 1611 | * [express.js syntax](https://expressjs.com/en/guide/routing.html#route-paths"). 1612 | * 1613 | * * `children` – an array of nested routes or a function that provides this 1614 | * array at the render time. The function can be synchronous or asynchronous: 1615 | * in the latter case the render is delayed until the returned promise is 1616 | * resolved. The `children` function is executed every time when this route is 1617 | * being rendered. This allows for dynamic route structures (e.g. backend-defined), 1618 | * but it might have a performance impact as well. In order to avoid calling 1619 | * the function on subsequent renders, you can override the `children` property 1620 | * of the route object and save the calculated array there 1621 | * (via `context.route.children = [ route1, route2, ...];`). 1622 | * Parent routes are fully resolved before resolving the children. Children 1623 | * 'path' values are relative to the parent ones. 1624 | * 1625 | * * `action` – the action that is executed before the route is resolved. 1626 | * The value for this property should be a function, accepting `context` 1627 | * and `commands` parameters described below. If present, this function is 1628 | * always invoked first, disregarding of the other properties' presence. 1629 | * The action can return a result directly or within a `Promise`, which 1630 | * resolves to the result. If the action result is an `HTMLElement` instance, 1631 | * a `commands.component(name)` result, a `commands.redirect(path)` result, 1632 | * or a `context.next()` result, the current route resolution is finished, 1633 | * and other route config properties are ignored. 1634 | * See also **Route Actions** section in [Live Examples](#/classes/Router/demos/demo/index.html). 1635 | * 1636 | * * `redirect` – other route's path to redirect to. Passes all route parameters to the redirect target. 1637 | * The target route should also be defined. 1638 | * See also **Redirects** section in [Live Examples](#/classes/Router/demos/demo/index.html). 1639 | * 1640 | * * `bundle` – string containing the path to `.js` or `.mjs` bundle to load before resolving the route, 1641 | * or the object with "module" and "nomodule" keys referring to different bundles. 1642 | * Each bundle is only loaded once. If "module" and "nomodule" are set, only one bundle is loaded, 1643 | * depending on whether the browser supports ES modules or not. 1644 | * The property is ignored when either an `action` returns the result or `redirect` property is present. 1645 | * Any error, e.g. 404 while loading bundle will cause route resolution to throw. 1646 | * See also **Code Splitting** section in [Live Examples](#/classes/Router/demos/demo/index.html). 1647 | * 1648 | * * `component` – the tag name of the Web Component to resolve the route to. 1649 | * The property is ignored when either an `action` returns the result or `redirect` property is present. 1650 | * If route contains the `component` property (or an action that return a component) 1651 | * and its child route also contains the `component` property, child route's component 1652 | * will be rendered as a light dom child of a parent component. 1653 | * 1654 | * * `name` – the string name of the route to use in the 1655 | * [`router.urlForName(name, params)`](#/classes/Router#method-urlForName) 1656 | * navigation helper method. 1657 | * 1658 | * For any route function (`action`, `children`) defined, the corresponding `route` object is available inside the callback 1659 | * through the `this` reference. If you need to access it, make sure you define the callback as a non-arrow function 1660 | * because arrow functions do not have their own `this` reference. 1661 | * 1662 | * `context` object that is passed to `action` function holds the following properties: 1663 | * * `context.pathname` – string with the pathname being resolved 1664 | * 1665 | * * `context.search` – search query string 1666 | * 1667 | * * `context.hash` – hash string 1668 | * 1669 | * * `context.params` – object with route parameters 1670 | * 1671 | * * `context.route` – object that holds the route that is currently being rendered. 1672 | * 1673 | * * `context.next()` – function for asynchronously getting the next route 1674 | * contents from the resolution chain (if any) 1675 | * 1676 | * `commands` object that is passed to `action` function has 1677 | * the following methods: 1678 | * 1679 | * * `commands.redirect(path)` – function that creates a redirect data 1680 | * for the path specified. 1681 | * 1682 | * * `commands.component(component)` – function that creates a new HTMLElement 1683 | * with current context. Note: the component created by this function is reused if visiting the same path twice in row. 1684 | * 1685 | * 1686 | * @param {!Array|!Router.Route} routes a single route or an array of those 1687 | * @param {?boolean} skipRender configure the router but skip rendering the 1688 | * route corresponding to the current `window.location` values 1689 | * 1690 | * @return {!Promise} 1691 | */ 1692 | setRoutes(routes, skipRender = false) { 1693 | this.__previousContext = undefined; 1694 | this.__urlForName = undefined; 1695 | super.setRoutes(routes); 1696 | if (!skipRender) { 1697 | this.__onNavigationEvent(); 1698 | } 1699 | return this.ready; 1700 | } 1701 | 1702 | /** 1703 | * Asynchronously resolves the given pathname and renders the resolved route 1704 | * component into the router outlet. If no router outlet is set at the time of 1705 | * calling this method, or at the time when the route resolution is completed, 1706 | * a `TypeError` is thrown. 1707 | * 1708 | * Returns a promise that is fulfilled with the router outlet DOM Node after 1709 | * the route component is created and inserted into the router outlet, or 1710 | * rejected if no route matches the given path. 1711 | * 1712 | * If another render pass is started before the previous one is completed, the 1713 | * result of the previous render pass is ignored. 1714 | * 1715 | * @param {!string|!{pathname: !string, search: ?string, hash: ?string}} pathnameOrContext 1716 | * the pathname to render or a context object with a `pathname` property, 1717 | * optional `search` and `hash` properties, and other properties 1718 | * to pass to the resolver. 1719 | * @param {boolean=} shouldUpdateHistory 1720 | * update browser history with the rendered location 1721 | * @return {!Promise} 1722 | */ 1723 | render(pathnameOrContext, shouldUpdateHistory) { 1724 | const renderId = ++this.__lastStartedRenderId; 1725 | const { 1726 | pathname, 1727 | search, 1728 | hash 1729 | } = isString(pathnameOrContext) ? {pathname: pathnameOrContext, search: '', hash: ''} : pathnameOrContext; 1730 | 1731 | // Find the first route that resolves to a non-empty result 1732 | this.ready = this.resolve({pathname, search, hash, __renderId: renderId}) 1733 | 1734 | // Process the result of this.resolve() and handle all special commands: 1735 | // (redirect / prevent / component). If the result is a 'component', 1736 | // then go deeper and build the entire chain of nested components matching 1737 | // the pathname. Also call all 'on before' callbacks along the way. 1738 | .then(context => this.__fullyResolveChain(context)) 1739 | 1740 | .then(context => { 1741 | if (this.__isLatestRender(context)) { 1742 | const previousContext = this.__previousContext; 1743 | 1744 | // Check if the render was prevented and make an early return in that case 1745 | if (context === previousContext) { 1746 | // Replace the history with the previous context 1747 | // to make sure the URL stays the same. 1748 | this.__updateBrowserHistory(previousContext, true); 1749 | return this.location; 1750 | } 1751 | 1752 | this.location = createLocation(context); 1753 | fireRouterEvent('location-changed', {router: this, location: this.location}); 1754 | 1755 | if (shouldUpdateHistory) { 1756 | this.__updateBrowserHistory(context, context.redirectFrom); 1757 | } 1758 | 1759 | // Skip detaching/re-attaching there are no render changes 1760 | if (context.__skipAttach) { 1761 | this.__copyUnchangedElements(context, previousContext); 1762 | this.__previousContext = context; 1763 | return this.location; 1764 | } 1765 | 1766 | this.__addAppearingContent(context, previousContext); 1767 | const animationDone = this.__animateIfNeeded(context); 1768 | 1769 | this.__runOnAfterEnterCallbacks(context); 1770 | this.__runOnAfterLeaveCallbacks(context, previousContext); 1771 | 1772 | return animationDone.then(() => { 1773 | if (this.__isLatestRender(context)) { 1774 | // If there is another render pass started after this one, 1775 | // the 'disappearing content' would be removed when the other 1776 | // render pass calls `this.__addAppearingContent()` 1777 | this.__removeDisappearingContent(); 1778 | 1779 | this.__previousContext = context; 1780 | return this.location; 1781 | } 1782 | }); 1783 | } 1784 | }) 1785 | .catch(error => { 1786 | if (renderId === this.__lastStartedRenderId) { 1787 | if (shouldUpdateHistory) { 1788 | this.__updateBrowserHistory({pathname, search, hash}); 1789 | } 1790 | removeDomNodes(this.__outlet && this.__outlet.children); 1791 | this.location = createLocation({pathname, resolver: this}); 1792 | fireRouterEvent('error', {router: this, error, pathname}); 1793 | throw error; 1794 | } 1795 | }); 1796 | return this.ready; 1797 | } 1798 | 1799 | // `topOfTheChainContextBeforeRedirects` is a context coming from Resolver.resolve(). 1800 | // It would contain a 'redirect' route or the first 'component' route that 1801 | // matched the pathname. There might be more child 'component' routes to be 1802 | // resolved and added into the chain. This method would find and add them. 1803 | // `contextBeforeRedirects` is the context containing such a child component 1804 | // route. It's only necessary when this method is called recursively (otherwise 1805 | // it's the same as the 'top of the chain' context). 1806 | // 1807 | // Apart from building the chain of child components, this method would also 1808 | // handle 'redirect' routes, call 'onBefore' callbacks and handle 'prevent' 1809 | // and 'redirect' callback results. 1810 | __fullyResolveChain(topOfTheChainContextBeforeRedirects, 1811 | contextBeforeRedirects = topOfTheChainContextBeforeRedirects) { 1812 | return this.__findComponentContextAfterAllRedirects(contextBeforeRedirects) 1813 | // `contextAfterRedirects` is always a context with an `HTMLElement` result 1814 | // In other cases the promise gets rejected and .then() is not called 1815 | .then(contextAfterRedirects => { 1816 | const redirectsHappened = contextAfterRedirects !== contextBeforeRedirects; 1817 | const topOfTheChainContextAfterRedirects = 1818 | redirectsHappened ? contextAfterRedirects : topOfTheChainContextBeforeRedirects; 1819 | return contextAfterRedirects.next() 1820 | .then(nextChildContext => { 1821 | if (nextChildContext === null || nextChildContext === notFoundResult) { 1822 | const matchedPath = getPathnameForRouter( 1823 | getMatchedPath(contextAfterRedirects.chain), 1824 | contextAfterRedirects.resolver 1825 | ); 1826 | if (matchedPath !== contextAfterRedirects.pathname) { 1827 | throw getNotFoundError(topOfTheChainContextAfterRedirects); 1828 | } 1829 | } 1830 | return nextChildContext && nextChildContext !== notFoundResult 1831 | ? this.__fullyResolveChain(topOfTheChainContextAfterRedirects, nextChildContext) 1832 | : this.__amendWithOnBeforeCallbacks(contextAfterRedirects); 1833 | }); 1834 | }); 1835 | } 1836 | 1837 | __findComponentContextAfterAllRedirects(context) { 1838 | const result = context.result; 1839 | if (result instanceof HTMLElement) { 1840 | renderElement(context, result); 1841 | return Promise.resolve(context); 1842 | } else if (result.redirect) { 1843 | return this.__redirect(result.redirect, context.__redirectCount, context.__renderId) 1844 | .then(context => this.__findComponentContextAfterAllRedirects(context)); 1845 | } else if (result instanceof Error) { 1846 | return Promise.reject(result); 1847 | } else { 1848 | return Promise.reject( 1849 | new Error( 1850 | log( 1851 | `Invalid route resolution result for path "${context.pathname}". ` + 1852 | `Expected redirect object or HTML element, but got: "${logValue(result)}". ` + 1853 | `Double check the action return value for the route.` 1854 | ) 1855 | )); 1856 | } 1857 | } 1858 | 1859 | __amendWithOnBeforeCallbacks(contextWithFullChain) { 1860 | return this.__runOnBeforeCallbacks(contextWithFullChain).then(amendedContext => { 1861 | if (amendedContext === this.__previousContext || amendedContext === contextWithFullChain) { 1862 | return amendedContext; 1863 | } 1864 | return this.__fullyResolveChain(amendedContext); 1865 | }); 1866 | } 1867 | 1868 | __runOnBeforeCallbacks(newContext) { 1869 | const previousContext = this.__previousContext || {}; 1870 | const previousChain = previousContext.chain || []; 1871 | const newChain = newContext.chain; 1872 | 1873 | let callbacks = Promise.resolve(); 1874 | const prevent = () => ({cancel: true}); 1875 | const redirect = (pathname) => createRedirect(newContext, pathname); 1876 | 1877 | newContext.__divergedChainIndex = 0; 1878 | newContext.__skipAttach = false; 1879 | if (previousChain.length) { 1880 | for (let i = 0; i < Math.min(previousChain.length, newChain.length); i = ++newContext.__divergedChainIndex) { 1881 | if (previousChain[i].route !== newChain[i].route 1882 | || previousChain[i].path !== newChain[i].path && previousChain[i].element !== newChain[i].element 1883 | || !this.__isReusableElement(previousChain[i].element, newChain[i].element)) { 1884 | break; 1885 | } 1886 | } 1887 | 1888 | // Skip re-attaching and notifications if element and chain do not change 1889 | newContext.__skipAttach = 1890 | // Same route chain 1891 | newChain.length === previousChain.length && newContext.__divergedChainIndex == newChain.length && 1892 | // Same element 1893 | this.__isReusableElement(newContext.result, previousContext.result); 1894 | 1895 | if (newContext.__skipAttach) { 1896 | // execute onBeforeLeave for changed segment element when skipping attach 1897 | for (let i = newChain.length - 1; i >= 0; i--) { 1898 | if (previousChain[i].path !== newChain[i].path) { 1899 | callbacks = this.__runOnBeforeLeaveCallbacks(callbacks, newContext, {prevent}, previousChain[i]); 1900 | } 1901 | } 1902 | // execute onBeforeEnter for changed segment element when skipping attach 1903 | for (let i = 0; i < newChain.length; i++) { 1904 | if (previousChain[i].path !== newChain[i].path) { 1905 | callbacks = this.__runOnBeforeEnterCallbacks(callbacks, newContext, {prevent, redirect}, newChain[i]); 1906 | } 1907 | } 1908 | } else { 1909 | // execute onBeforeLeave when NOT skipping attach 1910 | for (let i = previousChain.length - 1; i >= newContext.__divergedChainIndex; i--) { 1911 | callbacks = this.__runOnBeforeLeaveCallbacks(callbacks, newContext, {prevent}, previousChain[i]); 1912 | } 1913 | } 1914 | } 1915 | // execute onBeforeEnter when NOT skipping attach 1916 | for (let i = newContext.__divergedChainIndex; !newContext.__skipAttach && i < newChain.length; i++) { 1917 | callbacks = this.__runOnBeforeEnterCallbacks(callbacks, newContext, {prevent, redirect}, newChain[i]); 1918 | } 1919 | return callbacks.then(amendmentResult => { 1920 | if (amendmentResult) { 1921 | if (amendmentResult.cancel) { 1922 | this.__previousContext.__renderId = newContext.__renderId; 1923 | return this.__previousContext; 1924 | } 1925 | if (amendmentResult.redirect) { 1926 | return this.__redirect(amendmentResult.redirect, newContext.__redirectCount, newContext.__renderId); 1927 | } 1928 | } 1929 | return newContext; 1930 | }); 1931 | } 1932 | 1933 | __runOnBeforeLeaveCallbacks(callbacks, newContext, commands, chainElement) { 1934 | const location = createLocation(newContext); 1935 | return callbacks.then(result => { 1936 | if (this.__isLatestRender(newContext)) { 1937 | const afterLeaveFunction = amend('onBeforeLeave', [location, commands, this], chainElement.element); 1938 | return afterLeaveFunction(result); 1939 | } 1940 | }).then(result => { 1941 | if (!(result || {}).redirect) { 1942 | return result; 1943 | } 1944 | }); 1945 | } 1946 | 1947 | __runOnBeforeEnterCallbacks(callbacks, newContext, commands, chainElement) { 1948 | const location = createLocation(newContext, chainElement.route); 1949 | return callbacks.then(result => { 1950 | if (this.__isLatestRender(newContext)) { 1951 | const beforeEnterFunction = amend('onBeforeEnter', [location, commands, this], chainElement.element); 1952 | return beforeEnterFunction(result); 1953 | } 1954 | }); 1955 | } 1956 | 1957 | __isReusableElement(element, otherElement) { 1958 | if (element && otherElement) { 1959 | return this.__createdByRouter.get(element) && this.__createdByRouter.get(otherElement) 1960 | ? element.localName === otherElement.localName 1961 | : element === otherElement; 1962 | } 1963 | return false; 1964 | } 1965 | 1966 | __isLatestRender(context) { 1967 | return context.__renderId === this.__lastStartedRenderId; 1968 | } 1969 | 1970 | __redirect(redirectData, counter, renderId) { 1971 | if (counter > MAX_REDIRECT_COUNT) { 1972 | throw new Error(log(`Too many redirects when rendering ${redirectData.from}`)); 1973 | } 1974 | 1975 | return this.resolve({ 1976 | pathname: this.urlForPath( 1977 | redirectData.pathname, 1978 | redirectData.params 1979 | ), 1980 | redirectFrom: redirectData.from, 1981 | __redirectCount: (counter || 0) + 1, 1982 | __renderId: renderId 1983 | }); 1984 | } 1985 | 1986 | __ensureOutlet(outlet = this.__outlet) { 1987 | if (!(outlet instanceof Node)) { 1988 | throw new TypeError(log(`Expected router outlet to be a valid DOM Node (but got ${outlet})`)); 1989 | } 1990 | } 1991 | 1992 | __updateBrowserHistory({pathname, search = '', hash = ''}, replace) { 1993 | if (window.location.pathname !== pathname 1994 | || window.location.search !== search 1995 | || window.location.hash !== hash 1996 | ) { 1997 | const changeState = replace ? 'replaceState' : 'pushState'; 1998 | window.history[changeState](null, document.title, pathname + search + hash); 1999 | window.dispatchEvent(new PopStateEvent('popstate', {state: 'vaadin-router-ignore'})); 2000 | } 2001 | } 2002 | 2003 | __copyUnchangedElements(context, previousContext) { 2004 | // Find the deepest common parent between the last and the new component 2005 | // chains. Update references for the unchanged elements in the new chain 2006 | let deepestCommonParent = this.__outlet; 2007 | for (let i = 0; i < context.__divergedChainIndex; i++) { 2008 | const unchangedElement = previousContext && previousContext.chain[i].element; 2009 | if (unchangedElement) { 2010 | if (unchangedElement.parentNode === deepestCommonParent) { 2011 | context.chain[i].element = unchangedElement; 2012 | deepestCommonParent = unchangedElement; 2013 | } else { 2014 | break; 2015 | } 2016 | } 2017 | } 2018 | return deepestCommonParent; 2019 | } 2020 | 2021 | __addAppearingContent(context, previousContext) { 2022 | this.__ensureOutlet(); 2023 | 2024 | // If the previous 'entering' animation has not completed yet, 2025 | // stop it and remove that content from the DOM before adding new one. 2026 | this.__removeAppearingContent(); 2027 | 2028 | // Copy reusable elements from the previousContext to current 2029 | const deepestCommonParent = this.__copyUnchangedElements(context, previousContext); 2030 | 2031 | // Keep two lists of DOM elements: 2032 | // - those that should be removed once the transition animation is over 2033 | // - and those that should remain 2034 | this.__appearingContent = []; 2035 | this.__disappearingContent = Array 2036 | .from(deepestCommonParent.children) 2037 | .filter( 2038 | // Only remove layout content that was added by router 2039 | e => this.__addedByRouter.get(e) && 2040 | // Do not remove the result element to avoid flickering 2041 | e !== context.result); 2042 | 2043 | // Add new elements (starting after the deepest common parent) to the DOM. 2044 | // That way only the components that are actually different between the two 2045 | // locations are added to the DOM (and those that are common remain in the 2046 | // DOM without first removing and then adding them again). 2047 | let parentElement = deepestCommonParent; 2048 | for (let i = context.__divergedChainIndex; i < context.chain.length; i++) { 2049 | const elementToAdd = context.chain[i].element; 2050 | if (elementToAdd) { 2051 | parentElement.appendChild(elementToAdd); 2052 | this.__addedByRouter.set(elementToAdd, true); 2053 | if (parentElement === deepestCommonParent) { 2054 | this.__appearingContent.push(elementToAdd); 2055 | } 2056 | parentElement = elementToAdd; 2057 | } 2058 | } 2059 | } 2060 | 2061 | __removeDisappearingContent() { 2062 | if (this.__disappearingContent) { 2063 | removeDomNodes(this.__disappearingContent); 2064 | } 2065 | this.__disappearingContent = null; 2066 | this.__appearingContent = null; 2067 | } 2068 | 2069 | __removeAppearingContent() { 2070 | if (this.__disappearingContent && this.__appearingContent) { 2071 | removeDomNodes(this.__appearingContent); 2072 | this.__disappearingContent = null; 2073 | this.__appearingContent = null; 2074 | } 2075 | } 2076 | 2077 | __runOnAfterLeaveCallbacks(currentContext, targetContext) { 2078 | if (!targetContext) { 2079 | return; 2080 | } 2081 | 2082 | // REVERSE iteration: from Z to A 2083 | for (let i = targetContext.chain.length - 1; i >= currentContext.__divergedChainIndex; i--) { 2084 | if (!this.__isLatestRender(currentContext)) { 2085 | break; 2086 | } 2087 | const currentComponent = targetContext.chain[i].element; 2088 | if (!currentComponent) { 2089 | continue; 2090 | } 2091 | try { 2092 | const location = createLocation(currentContext); 2093 | runCallbackIfPossible( 2094 | currentComponent.onAfterLeave, 2095 | [location, {}, targetContext.resolver], 2096 | currentComponent); 2097 | } finally { 2098 | if (this.__disappearingContent.indexOf(currentComponent) > -1) { 2099 | removeDomNodes(currentComponent.children); 2100 | } 2101 | } 2102 | } 2103 | } 2104 | 2105 | __runOnAfterEnterCallbacks(currentContext) { 2106 | // forward iteration: from A to Z 2107 | for (let i = currentContext.__divergedChainIndex; i < currentContext.chain.length; i++) { 2108 | if (!this.__isLatestRender(currentContext)) { 2109 | break; 2110 | } 2111 | const currentComponent = currentContext.chain[i].element || {}; 2112 | const location = createLocation(currentContext, currentContext.chain[i].route); 2113 | runCallbackIfPossible( 2114 | currentComponent.onAfterEnter, 2115 | [location, {}, currentContext.resolver], 2116 | currentComponent); 2117 | } 2118 | } 2119 | 2120 | __animateIfNeeded(context) { 2121 | const from = (this.__disappearingContent || [])[0]; 2122 | const to = (this.__appearingContent || [])[0]; 2123 | const promises = []; 2124 | 2125 | const chain = context.chain; 2126 | let config; 2127 | for (let i = chain.length; i > 0; i--) { 2128 | if (chain[i - 1].route.animate) { 2129 | config = chain[i - 1].route.animate; 2130 | break; 2131 | } 2132 | } 2133 | 2134 | if (from && to && config) { 2135 | const leave = isObject(config) && config.leave || 'leaving'; 2136 | const enter = isObject(config) && config.enter || 'entering'; 2137 | promises.push(animate(from, leave)); 2138 | promises.push(animate(to, enter)); 2139 | } 2140 | 2141 | return Promise.all(promises).then(() => context); 2142 | } 2143 | 2144 | /** 2145 | * Subscribes this instance to navigation events on the `window`. 2146 | * 2147 | * NOTE: beware of resource leaks. For as long as a router instance is 2148 | * subscribed to navigation events, it won't be garbage collected. 2149 | */ 2150 | subscribe() { 2151 | window.addEventListener('vaadin-router-go', this.__navigationEventHandler); 2152 | } 2153 | 2154 | /** 2155 | * Removes the subscription to navigation events created in the `subscribe()` 2156 | * method. 2157 | */ 2158 | unsubscribe() { 2159 | window.removeEventListener('vaadin-router-go', this.__navigationEventHandler); 2160 | } 2161 | 2162 | __onNavigationEvent(event) { 2163 | const {pathname, search, hash} = event ? event.detail : window.location; 2164 | if (isString(this.__normalizePathname(pathname))) { 2165 | if (event && event.preventDefault) { 2166 | event.preventDefault(); 2167 | } 2168 | this.render({pathname, search, hash}, true); 2169 | } 2170 | } 2171 | 2172 | /** 2173 | * Configures what triggers Router navigation events: 2174 | * - `POPSTATE`: popstate events on the current `window` 2175 | * - `CLICK`: click events on `` links leading to the current page 2176 | * 2177 | * This method is invoked with the pre-configured values when creating a new Router instance. 2178 | * By default, both `POPSTATE` and `CLICK` are enabled. This setup is expected to cover most of the use cases. 2179 | * 2180 | * See the `router-config.js` for the default navigation triggers config. Based on it, you can 2181 | * create the own one and only import the triggers you need, instead of pulling in all the code, 2182 | * e.g. if you want to handle `click` differently. 2183 | * 2184 | * See also **Navigation Triggers** section in [Live Examples](#/classes/Router/demos/demo/index.html). 2185 | * 2186 | * @param {...Router.NavigationTrigger} triggers 2187 | */ 2188 | static setTriggers(...triggers) { 2189 | setNavigationTriggers(triggers); 2190 | } 2191 | 2192 | /** 2193 | * Generates a URL for the route with the given name, optionally performing 2194 | * substitution of parameters. 2195 | * 2196 | * The route is searched in all the Router instances subscribed to 2197 | * navigation events. 2198 | * 2199 | * **Note:** For child route names, only array children are considered. 2200 | * It is not possible to generate URLs using a name for routes set with 2201 | * a children function. 2202 | * 2203 | * @function urlForName 2204 | * @param {!string} name the route name or the route’s `component` name. 2205 | * @param {Router.Params=} params Optional object with route path parameters. 2206 | * Named parameters are passed by name (`params[name] = value`), unnamed 2207 | * parameters are passed by index (`params[index] = value`). 2208 | * 2209 | * @return {string} 2210 | */ 2211 | urlForName(name, params) { 2212 | if (!this.__urlForName) { 2213 | this.__urlForName = generateUrls(this); 2214 | } 2215 | return getPathnameForRouter( 2216 | this.__urlForName(name, params), 2217 | this 2218 | ); 2219 | } 2220 | 2221 | /** 2222 | * Generates a URL for the given route path, optionally performing 2223 | * substitution of parameters. 2224 | * 2225 | * @param {!string} path string route path declared in [express.js syntax](https://expressjs.com/en/guide/routing.html#route-paths"). 2226 | * @param {Router.Params=} params Optional object with route path parameters. 2227 | * Named parameters are passed by name (`params[name] = value`), unnamed 2228 | * parameters are passed by index (`params[index] = value`). 2229 | * 2230 | * @return {string} 2231 | */ 2232 | urlForPath(path, params) { 2233 | return getPathnameForRouter( 2234 | Router.pathToRegexp.compile(path)(params), 2235 | this 2236 | ); 2237 | } 2238 | 2239 | /** 2240 | * Triggers navigation to a new path. Returns a boolean without waiting until 2241 | * the navigation is complete. Returns `true` if at least one `Router` 2242 | * has handled the navigation (was subscribed and had `baseUrl` matching 2243 | * the `pathname` argument), otherwise returns `false`. 2244 | * 2245 | * @param {!string} pathname a new in-app path 2246 | * @return {boolean} 2247 | */ 2248 | static go(pathname) { 2249 | return fireRouterEvent('go', {pathname}); 2250 | } 2251 | } 2252 | 2253 | const DEV_MODE_CODE_REGEXP = 2254 | /\/\*\*\s+vaadin-dev-mode:start([\s\S]*)vaadin-dev-mode:end\s+\*\*\//i; 2255 | 2256 | const FlowClients = window.Vaadin && window.Vaadin.Flow && window.Vaadin.Flow.clients; 2257 | 2258 | function isMinified() { 2259 | function test() { 2260 | /** vaadin-dev-mode:start 2261 | return false; 2262 | vaadin-dev-mode:end **/ 2263 | return true; 2264 | } 2265 | return uncommentAndRun(test); 2266 | } 2267 | 2268 | function isDevelopmentMode() { 2269 | try { 2270 | if (isForcedDevelopmentMode()) { 2271 | return true; 2272 | } 2273 | 2274 | if (!isLocalhost()) { 2275 | return false; 2276 | } 2277 | 2278 | if (FlowClients) { 2279 | return !isFlowProductionMode(); 2280 | } 2281 | 2282 | return !isMinified(); 2283 | } catch (e) { 2284 | // Some error in this code, assume production so no further actions will be taken 2285 | return false; 2286 | } 2287 | } 2288 | 2289 | function isForcedDevelopmentMode() { 2290 | return localStorage.getItem("vaadin.developmentmode.force"); 2291 | } 2292 | 2293 | function isLocalhost() { 2294 | return (["localhost","127.0.0.1"].indexOf(window.location.hostname) >= 0); 2295 | } 2296 | 2297 | function isFlowProductionMode() { 2298 | if (FlowClients) { 2299 | const productionModeApps = Object.keys(FlowClients) 2300 | .map(key => FlowClients[key]) 2301 | .filter(client => client.productionMode); 2302 | if (productionModeApps.length > 0) { 2303 | return true; 2304 | } 2305 | } 2306 | return false; 2307 | } 2308 | 2309 | function uncommentAndRun(callback, args) { 2310 | if (typeof callback !== 'function') { 2311 | return; 2312 | } 2313 | 2314 | const match = DEV_MODE_CODE_REGEXP.exec(callback.toString()); 2315 | if (match) { 2316 | try { 2317 | // requires CSP: script-src 'unsafe-eval' 2318 | callback = new Function(match[1]); 2319 | } catch (e) { 2320 | // eat the exception 2321 | console.log('vaadin-development-mode-detector: uncommentAndRun() failed', e); 2322 | } 2323 | } 2324 | 2325 | return callback(args); 2326 | } 2327 | 2328 | // A guard against polymer-modulizer removing the window.Vaadin 2329 | // initialization above. 2330 | window['Vaadin'] = window['Vaadin'] || {}; 2331 | 2332 | /** 2333 | * Inspects the source code of the given `callback` function for 2334 | * specially-marked _commented_ code. If such commented code is found in the 2335 | * callback source, uncomments and runs that code instead of the callback 2336 | * itself. Otherwise runs the callback as is. 2337 | * 2338 | * The optional arguments are passed into the callback / uncommented code, 2339 | * the result is returned. 2340 | * 2341 | * See the `isMinified()` function source code in this file for an example. 2342 | * 2343 | */ 2344 | const runIfDevelopmentMode = function(callback, args) { 2345 | if (window.Vaadin.developmentMode) { 2346 | return uncommentAndRun(callback, args); 2347 | } 2348 | }; 2349 | 2350 | if (window.Vaadin.developmentMode === undefined) { 2351 | window.Vaadin.developmentMode = isDevelopmentMode(); 2352 | } 2353 | 2354 | /* This file is autogenerated from src/vaadin-usage-statistics.tpl.html */ 2355 | 2356 | function maybeGatherAndSendStats() { 2357 | /** vaadin-dev-mode:start 2358 | (function () { 2359 | 'use strict'; 2360 | 2361 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 2362 | return typeof obj; 2363 | } : function (obj) { 2364 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 2365 | }; 2366 | 2367 | var classCallCheck = function (instance, Constructor) { 2368 | if (!(instance instanceof Constructor)) { 2369 | throw new TypeError("Cannot call a class as a function"); 2370 | } 2371 | }; 2372 | 2373 | var createClass = function () { 2374 | function defineProperties(target, props) { 2375 | for (var i = 0; i < props.length; i++) { 2376 | var descriptor = props[i]; 2377 | descriptor.enumerable = descriptor.enumerable || false; 2378 | descriptor.configurable = true; 2379 | if ("value" in descriptor) descriptor.writable = true; 2380 | Object.defineProperty(target, descriptor.key, descriptor); 2381 | } 2382 | } 2383 | 2384 | return function (Constructor, protoProps, staticProps) { 2385 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 2386 | if (staticProps) defineProperties(Constructor, staticProps); 2387 | return Constructor; 2388 | }; 2389 | }(); 2390 | 2391 | var getPolymerVersion = function getPolymerVersion() { 2392 | return window.Polymer && window.Polymer.version; 2393 | }; 2394 | 2395 | var StatisticsGatherer = function () { 2396 | function StatisticsGatherer(logger) { 2397 | classCallCheck(this, StatisticsGatherer); 2398 | 2399 | this.now = new Date().getTime(); 2400 | this.logger = logger; 2401 | } 2402 | 2403 | createClass(StatisticsGatherer, [{ 2404 | key: 'frameworkVersionDetectors', 2405 | value: function frameworkVersionDetectors() { 2406 | return { 2407 | 'Flow': function Flow() { 2408 | if (window.Vaadin && window.Vaadin.Flow && window.Vaadin.Flow.clients) { 2409 | var flowVersions = Object.keys(window.Vaadin.Flow.clients).map(function (key) { 2410 | return window.Vaadin.Flow.clients[key]; 2411 | }).filter(function (client) { 2412 | return client.getVersionInfo; 2413 | }).map(function (client) { 2414 | return client.getVersionInfo().flow; 2415 | }); 2416 | if (flowVersions.length > 0) { 2417 | return flowVersions[0]; 2418 | } 2419 | } 2420 | }, 2421 | 'Vaadin Framework': function VaadinFramework() { 2422 | if (window.vaadin && window.vaadin.clients) { 2423 | var frameworkVersions = Object.values(window.vaadin.clients).filter(function (client) { 2424 | return client.getVersionInfo; 2425 | }).map(function (client) { 2426 | return client.getVersionInfo().vaadinVersion; 2427 | }); 2428 | if (frameworkVersions.length > 0) { 2429 | return frameworkVersions[0]; 2430 | } 2431 | } 2432 | }, 2433 | 'AngularJs': function AngularJs() { 2434 | if (window.angular && window.angular.version && window.angular.version) { 2435 | return window.angular.version.full; 2436 | } 2437 | }, 2438 | 'Angular': function Angular() { 2439 | if (window.ng) { 2440 | var tags = document.querySelectorAll("[ng-version]"); 2441 | if (tags.length > 0) { 2442 | return tags[0].getAttribute("ng-version"); 2443 | } 2444 | return "Unknown"; 2445 | } 2446 | }, 2447 | 'Backbone.js': function BackboneJs() { 2448 | if (window.Backbone) { 2449 | return window.Backbone.VERSION; 2450 | } 2451 | }, 2452 | 'React': function React() { 2453 | var reactSelector = '[data-reactroot], [data-reactid]'; 2454 | if (!!document.querySelector(reactSelector)) { 2455 | // React does not publish the version by default 2456 | return "unknown"; 2457 | } 2458 | }, 2459 | 'Ember': function Ember() { 2460 | if (window.Em && window.Em.VERSION) { 2461 | return window.Em.VERSION; 2462 | } else if (window.Ember && window.Ember.VERSION) { 2463 | return window.Ember.VERSION; 2464 | } 2465 | }, 2466 | 'jQuery': function (_jQuery) { 2467 | function jQuery() { 2468 | return _jQuery.apply(this, arguments); 2469 | } 2470 | 2471 | jQuery.toString = function () { 2472 | return _jQuery.toString(); 2473 | }; 2474 | 2475 | return jQuery; 2476 | }(function () { 2477 | if (typeof jQuery === 'function' && jQuery.prototype.jquery !== undefined) { 2478 | return jQuery.prototype.jquery; 2479 | } 2480 | }), 2481 | 'Polymer': function Polymer() { 2482 | var version = getPolymerVersion(); 2483 | if (version) { 2484 | return version; 2485 | } 2486 | }, 2487 | 'LitElement': function LitElement() { 2488 | var version = window.litElementVersions && window.litElementVersions[0]; 2489 | if (version) { 2490 | return version; 2491 | } 2492 | }, 2493 | 'LitHtml': function LitHtml() { 2494 | var version = window.litHtmlVersions && window.litHtmlVersions[0]; 2495 | if (version) { 2496 | return version; 2497 | } 2498 | }, 2499 | 'Vue.js': function VueJs() { 2500 | if (window.Vue) { 2501 | return window.Vue.version; 2502 | } 2503 | } 2504 | }; 2505 | } 2506 | }, { 2507 | key: 'getUsedVaadinElements', 2508 | value: function getUsedVaadinElements(elements) { 2509 | var version = getPolymerVersion(); 2510 | var elementClasses = void 0; 2511 | if (version && version.indexOf('2') === 0) { 2512 | // Polymer 2: components classes are stored in window.Vaadin 2513 | elementClasses = Object.keys(window.Vaadin).map(function (c) { 2514 | return window.Vaadin[c]; 2515 | }).filter(function (c) { 2516 | return c.is; 2517 | }); 2518 | } else { 2519 | // Polymer 3: components classes are stored in window.Vaadin.registrations 2520 | elementClasses = window.Vaadin.registrations || []; 2521 | } 2522 | elementClasses.forEach(function (klass) { 2523 | var version = klass.version ? klass.version : "0.0.0"; 2524 | elements[klass.is] = { version: version }; 2525 | }); 2526 | } 2527 | }, { 2528 | key: 'getUsedVaadinThemes', 2529 | value: function getUsedVaadinThemes(themes) { 2530 | ['Lumo', 'Material'].forEach(function (themeName) { 2531 | var theme; 2532 | var version = getPolymerVersion(); 2533 | if (version && version.indexOf('2') === 0) { 2534 | // Polymer 2: themes are stored in window.Vaadin 2535 | theme = window.Vaadin[themeName]; 2536 | } else { 2537 | // Polymer 3: themes are stored in custom element registry 2538 | theme = customElements.get('vaadin-' + themeName.toLowerCase() + '-styles'); 2539 | } 2540 | if (theme && theme.version) { 2541 | themes[themeName] = { version: theme.version }; 2542 | } 2543 | }); 2544 | } 2545 | }, { 2546 | key: 'getFrameworks', 2547 | value: function getFrameworks(frameworks) { 2548 | var detectors = this.frameworkVersionDetectors(); 2549 | Object.keys(detectors).forEach(function (framework) { 2550 | var detector = detectors[framework]; 2551 | try { 2552 | var version = detector(); 2553 | if (version) { 2554 | frameworks[framework] = { version: version }; 2555 | } 2556 | } catch (e) {} 2557 | }); 2558 | } 2559 | }, { 2560 | key: 'gather', 2561 | value: function gather(storage) { 2562 | var storedStats = storage.read(); 2563 | var gatheredStats = {}; 2564 | var types = ["elements", "frameworks", "themes"]; 2565 | 2566 | types.forEach(function (type) { 2567 | gatheredStats[type] = {}; 2568 | if (!storedStats[type]) { 2569 | storedStats[type] = {}; 2570 | } 2571 | }); 2572 | 2573 | var previousStats = JSON.stringify(storedStats); 2574 | 2575 | this.getUsedVaadinElements(gatheredStats.elements); 2576 | this.getFrameworks(gatheredStats.frameworks); 2577 | this.getUsedVaadinThemes(gatheredStats.themes); 2578 | 2579 | var now = this.now; 2580 | types.forEach(function (type) { 2581 | var keys = Object.keys(gatheredStats[type]); 2582 | keys.forEach(function (key) { 2583 | if (!storedStats[type][key] || _typeof(storedStats[type][key]) != _typeof({})) { 2584 | storedStats[type][key] = { firstUsed: now }; 2585 | } 2586 | // Discards any previously logged version number 2587 | storedStats[type][key].version = gatheredStats[type][key].version; 2588 | storedStats[type][key].lastUsed = now; 2589 | }); 2590 | }); 2591 | 2592 | var newStats = JSON.stringify(storedStats); 2593 | storage.write(newStats); 2594 | if (newStats != previousStats && Object.keys(storedStats).length > 0) { 2595 | this.logger.debug("New stats: " + newStats); 2596 | } 2597 | } 2598 | }]); 2599 | return StatisticsGatherer; 2600 | }(); 2601 | 2602 | var StatisticsStorage = function () { 2603 | function StatisticsStorage(key) { 2604 | classCallCheck(this, StatisticsStorage); 2605 | 2606 | this.key = key; 2607 | } 2608 | 2609 | createClass(StatisticsStorage, [{ 2610 | key: 'read', 2611 | value: function read() { 2612 | var localStorageStatsString = localStorage.getItem(this.key); 2613 | try { 2614 | return JSON.parse(localStorageStatsString ? localStorageStatsString : '{}'); 2615 | } catch (e) { 2616 | return {}; 2617 | } 2618 | } 2619 | }, { 2620 | key: 'write', 2621 | value: function write(data) { 2622 | localStorage.setItem(this.key, data); 2623 | } 2624 | }, { 2625 | key: 'clear', 2626 | value: function clear() { 2627 | localStorage.removeItem(this.key); 2628 | } 2629 | }, { 2630 | key: 'isEmpty', 2631 | value: function isEmpty() { 2632 | var storedStats = this.read(); 2633 | var empty = true; 2634 | Object.keys(storedStats).forEach(function (key) { 2635 | if (Object.keys(storedStats[key]).length > 0) { 2636 | empty = false; 2637 | } 2638 | }); 2639 | 2640 | return empty; 2641 | } 2642 | }]); 2643 | return StatisticsStorage; 2644 | }(); 2645 | 2646 | var StatisticsSender = function () { 2647 | function StatisticsSender(url, logger) { 2648 | classCallCheck(this, StatisticsSender); 2649 | 2650 | this.url = url; 2651 | this.logger = logger; 2652 | } 2653 | 2654 | createClass(StatisticsSender, [{ 2655 | key: 'send', 2656 | value: function send(data, errorHandler) { 2657 | var logger = this.logger; 2658 | 2659 | if (navigator.onLine === false) { 2660 | logger.debug("Offline, can't send"); 2661 | errorHandler(); 2662 | return; 2663 | } 2664 | logger.debug("Sending data to " + this.url); 2665 | 2666 | var req = new XMLHttpRequest(); 2667 | req.withCredentials = true; 2668 | req.addEventListener("load", function () { 2669 | // Stats sent, nothing more to do 2670 | logger.debug("Response: " + req.responseText); 2671 | }); 2672 | req.addEventListener("error", function () { 2673 | logger.debug("Send failed"); 2674 | errorHandler(); 2675 | }); 2676 | req.addEventListener("abort", function () { 2677 | logger.debug("Send aborted"); 2678 | errorHandler(); 2679 | }); 2680 | req.open("POST", this.url); 2681 | req.setRequestHeader("Content-Type", "application/json"); 2682 | req.send(data); 2683 | } 2684 | }]); 2685 | return StatisticsSender; 2686 | }(); 2687 | 2688 | var StatisticsLogger = function () { 2689 | function StatisticsLogger(id) { 2690 | classCallCheck(this, StatisticsLogger); 2691 | 2692 | this.id = id; 2693 | } 2694 | 2695 | createClass(StatisticsLogger, [{ 2696 | key: '_isDebug', 2697 | value: function _isDebug() { 2698 | return localStorage.getItem("vaadin." + this.id + ".debug"); 2699 | } 2700 | }, { 2701 | key: 'debug', 2702 | value: function debug(msg) { 2703 | if (this._isDebug()) { 2704 | console.info(this.id + ": " + msg); 2705 | } 2706 | } 2707 | }]); 2708 | return StatisticsLogger; 2709 | }(); 2710 | 2711 | var UsageStatistics = function () { 2712 | function UsageStatistics() { 2713 | classCallCheck(this, UsageStatistics); 2714 | 2715 | this.now = new Date(); 2716 | this.timeNow = this.now.getTime(); 2717 | this.gatherDelay = 10; // Delay between loading this file and gathering stats 2718 | this.initialDelay = 24 * 60 * 60; 2719 | 2720 | this.logger = new StatisticsLogger("statistics"); 2721 | this.storage = new StatisticsStorage("vaadin.statistics.basket"); 2722 | this.gatherer = new StatisticsGatherer(this.logger); 2723 | this.sender = new StatisticsSender("https://tools.vaadin.com/usage-stats/submit", this.logger); 2724 | } 2725 | 2726 | createClass(UsageStatistics, [{ 2727 | key: 'maybeGatherAndSend', 2728 | value: function maybeGatherAndSend() { 2729 | var _this = this; 2730 | 2731 | if (localStorage.getItem(UsageStatistics.optOutKey)) { 2732 | return; 2733 | } 2734 | this.gatherer.gather(this.storage); 2735 | setTimeout(function () { 2736 | _this.maybeSend(); 2737 | }, this.gatherDelay * 1000); 2738 | } 2739 | }, { 2740 | key: 'lottery', 2741 | value: function lottery() { 2742 | return Math.random() <= 0.05; 2743 | } 2744 | }, { 2745 | key: 'currentMonth', 2746 | value: function currentMonth() { 2747 | return this.now.getYear() * 12 + this.now.getMonth(); 2748 | } 2749 | }, { 2750 | key: 'maybeSend', 2751 | value: function maybeSend() { 2752 | var firstUse = Number(localStorage.getItem(UsageStatistics.firstUseKey)); 2753 | var monthProcessed = Number(localStorage.getItem(UsageStatistics.monthProcessedKey)); 2754 | 2755 | if (!firstUse) { 2756 | // Use a grace period to avoid interfering with tests, incognito mode etc 2757 | firstUse = this.timeNow; 2758 | localStorage.setItem(UsageStatistics.firstUseKey, firstUse); 2759 | } 2760 | 2761 | if (this.timeNow < firstUse + this.initialDelay * 1000) { 2762 | this.logger.debug("No statistics will be sent until the initial delay of " + this.initialDelay + "s has passed"); 2763 | return; 2764 | } 2765 | if (this.currentMonth() <= monthProcessed) { 2766 | this.logger.debug("This month has already been processed"); 2767 | return; 2768 | } 2769 | localStorage.setItem(UsageStatistics.monthProcessedKey, this.currentMonth()); 2770 | // Use random sampling 2771 | if (this.lottery()) { 2772 | this.logger.debug("Congratulations, we have a winner!"); 2773 | } else { 2774 | this.logger.debug("Sorry, no stats from you this time"); 2775 | return; 2776 | } 2777 | 2778 | this.send(); 2779 | } 2780 | }, { 2781 | key: 'send', 2782 | value: function send() { 2783 | // Ensure we have the latest data 2784 | this.gatherer.gather(this.storage); 2785 | 2786 | // Read, send and clean up 2787 | var data = this.storage.read(); 2788 | data["firstUse"] = Number(localStorage.getItem(UsageStatistics.firstUseKey)); 2789 | data["usageStatisticsVersion"] = UsageStatistics.version; 2790 | var info = 'This request contains usage statistics gathered from the application running in development mode. \n\nStatistics gathering is automatically disabled and excluded from production builds.\n\nFor details and to opt-out, see https://github.com/vaadin/vaadin-usage-statistics.\n\n\n\n'; 2791 | var self = this; 2792 | this.sender.send(info + JSON.stringify(data), function () { 2793 | // Revert the 'month processed' flag 2794 | localStorage.setItem(UsageStatistics.monthProcessedKey, self.currentMonth() - 1); 2795 | }); 2796 | } 2797 | }], [{ 2798 | key: 'version', 2799 | get: function get$1() { 2800 | return '2.0.8'; 2801 | } 2802 | }, { 2803 | key: 'firstUseKey', 2804 | get: function get$1() { 2805 | return 'vaadin.statistics.firstuse'; 2806 | } 2807 | }, { 2808 | key: 'monthProcessedKey', 2809 | get: function get$1() { 2810 | return 'vaadin.statistics.monthProcessed'; 2811 | } 2812 | }, { 2813 | key: 'optOutKey', 2814 | get: function get$1() { 2815 | return 'vaadin.statistics.optout'; 2816 | } 2817 | }]); 2818 | return UsageStatistics; 2819 | }(); 2820 | 2821 | try { 2822 | window.Vaadin = window.Vaadin || {}; 2823 | window.Vaadin.usageStatsChecker = window.Vaadin.usageStatsChecker || new UsageStatistics(); 2824 | window.Vaadin.usageStatsChecker.maybeGatherAndSend(); 2825 | } catch (e) { 2826 | // Intentionally ignored as this is not a problem in the app being developed 2827 | } 2828 | 2829 | }()); 2830 | 2831 | vaadin-dev-mode:end **/ 2832 | } 2833 | 2834 | const usageStatistics = function() { 2835 | if (typeof runIfDevelopmentMode === 'function') { 2836 | return runIfDevelopmentMode(maybeGatherAndSendStats); 2837 | } 2838 | }; 2839 | 2840 | window.Vaadin = window.Vaadin || {}; 2841 | window.Vaadin.registrations = window.Vaadin.registrations || []; 2842 | 2843 | window.Vaadin.registrations.push({ 2844 | is: '@vaadin/router', 2845 | version: '1.4.3', 2846 | }); 2847 | 2848 | usageStatistics(); 2849 | 2850 | Router.NavigationTrigger = {POPSTATE, CLICK}; 2851 | 2852 | export { Resolver, Router }; 2853 | -------------------------------------------------------------------------------- /demo/web_modules/import-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@adobe/lit-mobx": "./@adobe/lit-mobx.js", 4 | "lit-element": "./lit-element.js", 5 | "mobx": "./mobx.js" 6 | } 7 | } -------------------------------------------------------------------------------- /demo/web_modules/shader-doodle.js: -------------------------------------------------------------------------------- 1 | function _classCallCheck(instance, Constructor) { 2 | if (!(instance instanceof Constructor)) { 3 | throw new TypeError("Cannot call a class as a function"); 4 | } 5 | } 6 | 7 | function _defineProperties(target, props) { 8 | for (var i = 0; i < props.length; i++) { 9 | var descriptor = props[i]; 10 | descriptor.enumerable = descriptor.enumerable || false; 11 | descriptor.configurable = true; 12 | if ("value" in descriptor) descriptor.writable = true; 13 | Object.defineProperty(target, descriptor.key, descriptor); 14 | } 15 | } 16 | 17 | function _createClass(Constructor, protoProps, staticProps) { 18 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 19 | if (staticProps) _defineProperties(Constructor, staticProps); 20 | return Constructor; 21 | } 22 | 23 | function _defineProperty(obj, key, value) { 24 | if (key in obj) { 25 | Object.defineProperty(obj, key, { 26 | value: value, 27 | enumerable: true, 28 | configurable: true, 29 | writable: true 30 | }); 31 | } else { 32 | obj[key] = value; 33 | } 34 | 35 | return obj; 36 | } 37 | 38 | function _objectSpread(target) { 39 | for (var i = 1; i < arguments.length; i++) { 40 | var source = arguments[i] != null ? arguments[i] : {}; 41 | var ownKeys = Object.keys(source); 42 | 43 | if (typeof Object.getOwnPropertySymbols === 'function') { 44 | ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { 45 | return Object.getOwnPropertyDescriptor(source, sym).enumerable; 46 | })); 47 | } 48 | 49 | ownKeys.forEach(function (key) { 50 | _defineProperty(target, key, source[key]); 51 | }); 52 | } 53 | 54 | return target; 55 | } 56 | 57 | function _inherits(subClass, superClass) { 58 | if (typeof superClass !== "function" && superClass !== null) { 59 | throw new TypeError("Super expression must either be null or a function"); 60 | } 61 | 62 | subClass.prototype = Object.create(superClass && superClass.prototype, { 63 | constructor: { 64 | value: subClass, 65 | writable: true, 66 | configurable: true 67 | } 68 | }); 69 | if (superClass) _setPrototypeOf(subClass, superClass); 70 | } 71 | 72 | function _getPrototypeOf(o) { 73 | _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { 74 | return o.__proto__ || Object.getPrototypeOf(o); 75 | }; 76 | return _getPrototypeOf(o); 77 | } 78 | 79 | function _setPrototypeOf(o, p) { 80 | _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { 81 | o.__proto__ = p; 82 | return o; 83 | }; 84 | 85 | return _setPrototypeOf(o, p); 86 | } 87 | 88 | function isNativeReflectConstruct() { 89 | if (typeof Reflect === "undefined" || !Reflect.construct) return false; 90 | if (Reflect.construct.sham) return false; 91 | if (typeof Proxy === "function") return true; 92 | 93 | try { 94 | Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); 95 | return true; 96 | } catch (e) { 97 | return false; 98 | } 99 | } 100 | 101 | function _construct(Parent, args, Class) { 102 | if (isNativeReflectConstruct()) { 103 | _construct = Reflect.construct; 104 | } else { 105 | _construct = function _construct(Parent, args, Class) { 106 | var a = [null]; 107 | a.push.apply(a, args); 108 | var Constructor = Function.bind.apply(Parent, a); 109 | var instance = new Constructor(); 110 | if (Class) _setPrototypeOf(instance, Class.prototype); 111 | return instance; 112 | }; 113 | } 114 | 115 | return _construct.apply(null, arguments); 116 | } 117 | 118 | function _isNativeFunction(fn) { 119 | return Function.toString.call(fn).indexOf("[native code]") !== -1; 120 | } 121 | 122 | function _wrapNativeSuper(Class) { 123 | var _cache = typeof Map === "function" ? new Map() : undefined; 124 | 125 | _wrapNativeSuper = function _wrapNativeSuper(Class) { 126 | if (Class === null || !_isNativeFunction(Class)) return Class; 127 | 128 | if (typeof Class !== "function") { 129 | throw new TypeError("Super expression must either be null or a function"); 130 | } 131 | 132 | if (typeof _cache !== "undefined") { 133 | if (_cache.has(Class)) return _cache.get(Class); 134 | 135 | _cache.set(Class, Wrapper); 136 | } 137 | 138 | function Wrapper() { 139 | return _construct(Class, arguments, _getPrototypeOf(this).constructor); 140 | } 141 | 142 | Wrapper.prototype = Object.create(Class.prototype, { 143 | constructor: { 144 | value: Wrapper, 145 | enumerable: false, 146 | writable: true, 147 | configurable: true 148 | } 149 | }); 150 | return _setPrototypeOf(Wrapper, Class); 151 | }; 152 | 153 | return _wrapNativeSuper(Class); 154 | } 155 | 156 | function _assertThisInitialized(self) { 157 | if (self === void 0) { 158 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 159 | } 160 | 161 | return self; 162 | } 163 | 164 | function _possibleConstructorReturn(self, call) { 165 | if (call && (typeof call === "object" || typeof call === "function")) { 166 | return call; 167 | } 168 | 169 | return _assertThisInitialized(self); 170 | } 171 | 172 | var Template = { 173 | render: function render() { 174 | return "".concat(this.css(), "\n ").concat(this.html()); 175 | }, 176 | defaultVertexShader: function defaultVertexShader() { 177 | return "attribute vec2 position;\n \n void main() {\n gl_Position = vec4(position, 0.0, 1.0);\n }"; 178 | }, 179 | map: function map(scope) { 180 | return { 181 | canvas: scope.querySelector('canvas') 182 | }; 183 | }, 184 | html: function html(node) { 185 | return ""; 186 | }, 187 | css: function css() { 188 | return ""; 189 | } 190 | }; 191 | 192 | var CAMERA = 'camera'; 193 | var IMAGE = 'image'; 194 | var VIDEO = 'video'; 195 | var REPEAT = 0x2901; 196 | var CLAMP_TO_EDGE = 0x812f; 197 | var MIRRORED_REPEAT = 0x8370; 198 | var NEAREST = 0x2600; 199 | var LINEAR = 0x2601; 200 | var NEAREST_MIPMAP_NEAREST = 0x2700; 201 | var LINEAR_MIPMAP_NEAREST = 0x2701; 202 | var NEAREST_MIPMAP_LINEAR = 0x2702; 203 | var LINEAR_MIPMAP_LINEAR = 0x2703; 204 | var MAG_OPTIONS = { 205 | NEAREST: NEAREST, 206 | LINEAR: LINEAR 207 | }; 208 | 209 | var MIN_OPTIONS = _objectSpread({}, MAG_OPTIONS, { 210 | NEAREST_MIPMAP_NEAREST: NEAREST_MIPMAP_NEAREST, 211 | LINEAR_MIPMAP_NEAREST: LINEAR_MIPMAP_NEAREST, 212 | NEAREST_MIPMAP_LINEAR: NEAREST_MIPMAP_LINEAR, 213 | LINEAR_MIPMAP_LINEAR: LINEAR_MIPMAP_LINEAR 214 | }); 215 | 216 | var WRAP_OPTIONS = { 217 | REPEAT: REPEAT, 218 | MIRRORED_REPEAT: MIRRORED_REPEAT, 219 | CLAMP_TO_EDGE: CLAMP_TO_EDGE 220 | }; 221 | var PIXEL = new Uint8Array([0, 0, 0, 255]); 222 | var IMG_REG = /(\.jpg|\.jpeg|\.png|\.gif|\.bmp)$/i; 223 | 224 | var isImage = function isImage(s) { 225 | return IMG_REG.test(s); 226 | }; 227 | 228 | var VID_REG = /(\.mp4|\.3gp|\.webm|\.ogv)$/i; 229 | 230 | var isVideo = function isVideo(s) { 231 | return VID_REG.test(s); 232 | }; 233 | 234 | var floorPowerOfTwo = function floorPowerOfTwo(value) { 235 | return Math.pow(2, Math.floor(Math.log(value) / Math.LN2)); 236 | }; 237 | 238 | var isPow2 = function isPow2(value) { 239 | return !(value & value - 1) && !!value; 240 | }; 241 | 242 | var Texture = 243 | /*#__PURE__*/ 244 | function (_HTMLElement) { 245 | _inherits(Texture, _HTMLElement); 246 | 247 | _createClass(Texture, null, [{ 248 | key: "observedAttributes", 249 | get: function get() { 250 | return ['mag-filter', 'min-filter', 'name', 'src', 'wrap-s', 'wrap-t']; 251 | } 252 | }]); 253 | 254 | function Texture() { 255 | var _this; 256 | 257 | _classCallCheck(this, Texture); 258 | 259 | _this = _possibleConstructorReturn(this, _getPrototypeOf(Texture).call(this)); 260 | _this._imageOnload = _this._imageOnload.bind(_assertThisInitialized(_this)); 261 | return _this; 262 | } 263 | 264 | _createClass(Texture, [{ 265 | key: "disconnectedCallback", 266 | value: function disconnectedCallback() {// DELETE TEXTURE 267 | } 268 | }, { 269 | key: "init", 270 | value: function init(gl, program, index) { 271 | this._gl = gl; 272 | this._program = program; 273 | this._index = index; 274 | this._glTexture = this._gl.createTexture(); 275 | 276 | this._gl.bindTexture(this._gl.TEXTURE_2D, this._glTexture); 277 | 278 | this._setTexture(); 279 | 280 | this._location = this._gl.getUniformLocation(this._program, this.name); 281 | if (!this.src && !this.webcam) return; 282 | 283 | if (this.webcam) { 284 | this._setupCamera(); 285 | } else if (isVideo(this.src)) { 286 | this._setupVideo(); 287 | } else if (isImage(this.src)) { 288 | this._setupImage(); 289 | } 290 | } 291 | }, { 292 | key: "update", 293 | value: function update() { 294 | var gl = this._gl, 295 | texture = this._glTexture, 296 | index = this._index; 297 | if (!gl || !texture || typeof index !== 'number') return; 298 | gl.activeTexture(gl["TEXTURE".concat(index)]); 299 | gl.bindTexture(gl.TEXTURE_2D, texture); 300 | gl.uniform1i(this._location, index); 301 | 302 | if (this.shouldUpdate) { 303 | this._updateTexture(); 304 | } 305 | } 306 | }, { 307 | key: "_setTexture", 308 | value: function _setTexture(texture) { 309 | var gl = this._gl; 310 | if (!gl) return; 311 | var level = 0; 312 | var internalFormat = gl.RGBA; 313 | var width = 1; 314 | var height = 1; 315 | var border = 0; 316 | var srcFormat = gl.RGBA; 317 | var srcType = gl.UNSIGNED_BYTE; 318 | 319 | if (texture) { 320 | gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, texture); 321 | } else { 322 | gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, srcFormat, srcType, PIXEL); 323 | } 324 | } 325 | }, { 326 | key: "_setupImage", 327 | value: function _setupImage() { 328 | var _this2 = this; 329 | 330 | this.type = IMAGE; 331 | this._source = new Image(); 332 | this._source.crossOrigin = 'anonymous'; 333 | this._source.onload = this._imageOnload; 334 | 335 | this._source.onerror = function () { 336 | console.warn("failed loading src: ".concat(_this2.src)); 337 | }; 338 | 339 | this._source.src = this.src; 340 | } 341 | }, { 342 | key: "_imageOnload", 343 | value: function _imageOnload() { 344 | var gl = this._gl, 345 | img = this._source; 346 | 347 | if (!gl || !img || !(img instanceof HTMLImageElement || img instanceof HTMLCanvasElement || img instanceof ImageBitmap)) { 348 | return; 349 | } 350 | 351 | var isPowerOf2 = isPow2(img.width) && isPow2(img.height); 352 | var needsPowerOfTwo = this.wrapS !== CLAMP_TO_EDGE || this.wrapT !== CLAMP_TO_EDGE || this.minFilter !== NEAREST && this.minFilter !== LINEAR; 353 | 354 | if (needsPowerOfTwo && isPowerOf2 === false) { 355 | this.pow2canvas = this.pow2canvas || document.createElement('canvas'); 356 | this.pow2canvas.width = floorPowerOfTwo(img.width); 357 | this.pow2canvas.height = floorPowerOfTwo(img.height); 358 | var ctx = this.pow2canvas.getContext('2d'); 359 | ctx.drawImage(img, 0, 0, this.pow2canvas.width, this.pow2canvas.height); 360 | console.warn("Image is not power of two ".concat(img.width, " x ").concat(img.height, ". Resized to ").concat(this.pow2canvas.width, " x ").concat(this.pow2canvas.height, ";")); 361 | this._source = this.pow2canvas; 362 | isPowerOf2 = true; 363 | } 364 | 365 | this._updateTexture(); 366 | 367 | if (isPowerOf2 && this.minFilter !== NEAREST && this.minFilter !== LINEAR) { 368 | gl.generateMipmap(gl.TEXTURE_2D); 369 | } 370 | 371 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this.wrapS); 372 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this.wrapT); 373 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.minFilter); 374 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.magFilter); 375 | } 376 | }, { 377 | key: "_setupVideo", 378 | value: function _setupVideo() { 379 | var gl = this._gl; 380 | if (!gl) return; 381 | this.type = VIDEO; 382 | this._source = document.createElement('video'); 383 | this._source.autoplay = true; 384 | this._source.muted = true; 385 | this._source.loop = true; 386 | this._source.playsInline = true; 387 | this._source.crossOrigin = 'anonymous'; 388 | this._source.src = this.src; 389 | var wrapper = document.createElement('div'); 390 | wrapper.style.width = wrapper.style.height = '1px'; 391 | wrapper.style.overflow = 'hidden'; 392 | wrapper.style.position = 'absolute'; 393 | wrapper.style.opacity = '0'; 394 | wrapper.style.pointerEvents = 'none'; 395 | wrapper.style.zIndex = '-1000'; 396 | wrapper.appendChild(this._source); 397 | document.body.appendChild(wrapper); 398 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 399 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 400 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 401 | 402 | this._source.play(); 403 | } 404 | }, { 405 | key: "_setupCamera", 406 | value: function _setupCamera() { 407 | var _this3 = this; 408 | 409 | var gl = this._gl; 410 | if (!gl) return; 411 | this.type = CAMERA; 412 | var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; 413 | 414 | var start = function start(stream) { 415 | _this3._source = document.createElement('video'); 416 | _this3._source.width = 320; 417 | _this3._source.height = 240; 418 | _this3._source.autoplay = true; 419 | _this3._source.srcObject = stream; 420 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 421 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 422 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 423 | }; 424 | 425 | var init = function init() { 426 | navigator.mediaDevices.getUserMedia({ 427 | video: true 428 | }).then(start).catch(function (e) { 429 | return console.log(e.name + ': ' + e.message); 430 | }); 431 | }; 432 | 433 | var initLegacy = function initLegacy() { 434 | getUserMedia({ 435 | video: true 436 | }, start, function (e) { 437 | return e; 438 | }); 439 | }; 440 | 441 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 442 | init(); 443 | } else if (getUserMedia) { 444 | initLegacy(); 445 | } 446 | } 447 | }, { 448 | key: "_updateTexture", 449 | value: function _updateTexture() { 450 | var gl = this._gl, 451 | texture = this._glTexture, 452 | source = this._source; 453 | if (!gl || !texture || !source) return; 454 | gl.bindTexture(gl.TEXTURE_2D, texture); 455 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY); 456 | 457 | this._setTexture(source); 458 | } 459 | }, { 460 | key: "flipY", 461 | get: function get() { 462 | return true; 463 | } 464 | }, { 465 | key: "magFilter", 466 | get: function get() { 467 | return MAG_OPTIONS[this.getAttribute('mag-filter')] || LINEAR; 468 | } 469 | }, { 470 | key: "minFilter", 471 | get: function get() { 472 | return MIN_OPTIONS[this.getAttribute('min-filter')] || LINEAR_MIPMAP_LINEAR; 473 | } 474 | }, { 475 | key: "name", 476 | get: function get() { 477 | return this.getAttribute('name'); 478 | }, 479 | set: function set(val) { 480 | this.setAttribute('name', val); 481 | } 482 | }, { 483 | key: "shouldUpdate", 484 | get: function get() { 485 | return (this.type === CAMERA || this.type === VIDEO) && this._source instanceof HTMLVideoElement && this._source.readyState === this._source.HAVE_ENOUGH_DATA; 486 | } 487 | }, { 488 | key: "src", 489 | get: function get() { 490 | return this.getAttribute('src'); 491 | }, 492 | set: function set(val) { 493 | this.setAttribute('src', val); 494 | } 495 | }, { 496 | key: "webcam", 497 | get: function get() { 498 | return this.hasAttribute('webcam'); 499 | }, 500 | set: function set(cam) { 501 | if (cam) { 502 | this.setAttribute('webcam', ''); 503 | } else { 504 | this.removeAttribute('webcam'); 505 | } 506 | } 507 | }, { 508 | key: "wrapS", 509 | get: function get() { 510 | return WRAP_OPTIONS[this.getAttribute('wrap-s')] || REPEAT; 511 | } 512 | }, { 513 | key: "wrapT", 514 | get: function get() { 515 | return WRAP_OPTIONS[this.getAttribute('wrap-t')] || REPEAT; 516 | } 517 | }]); 518 | 519 | return Texture; 520 | }(_wrapNativeSuper(HTMLElement)); 521 | 522 | if (!customElements.get('sd-texture')) { 523 | customElements.define('sd-texture', Texture); 524 | } 525 | 526 | var SHADERTOY_IO = /\(\s*out\s+vec4\s+(\S+)\s*,\s*in\s+vec2\s+(\S+)\s*\)/; 527 | var UNNAMED_TEXTURE_PREFIX = 'u_texture_'; 528 | 529 | var ShaderDoodle = 530 | /*#__PURE__*/ 531 | function (_HTMLElement) { 532 | _inherits(ShaderDoodle, _HTMLElement); 533 | 534 | function ShaderDoodle() { 535 | var _this; 536 | 537 | _classCallCheck(this, ShaderDoodle); 538 | 539 | _this = _possibleConstructorReturn(this, _getPrototypeOf(ShaderDoodle).call(this)); 540 | _this.unnamedTextureIndex = 0; 541 | _this.shadow = _this.attachShadow({ 542 | mode: 'open' 543 | }); 544 | return _this; 545 | } 546 | 547 | _createClass(ShaderDoodle, [{ 548 | key: "connectedCallback", 549 | value: function connectedCallback() { 550 | var _this2 = this; 551 | 552 | this.mounted = true; 553 | setTimeout(function () { 554 | try { 555 | _this2.init(); 556 | } catch (e) { 557 | console.error(e && e.message || 'Error in shader-doodle.'); 558 | } 559 | }); 560 | } 561 | }, { 562 | key: "disconnectedCallback", 563 | value: function disconnectedCallback() { 564 | this.mounted = false; 565 | this.canvas.removeEventListener('mousedown', this.mouseDown); 566 | this.canvas.removeEventListener('mousemove', this.mouseMove); 567 | this.canvas.removeEventListener('mouseup', this.mouseUp); 568 | cancelAnimationFrame(this.animationFrame); 569 | } 570 | }, { 571 | key: "findShaders", 572 | value: function findShaders() { 573 | var shdrs = {}; 574 | 575 | for (var c = 0; c < this.children.length; c++) { 576 | switch (this.children[c].getAttribute('type')) { 577 | case 'x-shader/x-fragment': 578 | shdrs.fragmentShader = this.children[c].text; 579 | break; 580 | 581 | case 'x-shader/x-vertex': 582 | shdrs.vertexShader = this.children[c].text; 583 | break; 584 | } 585 | } 586 | 587 | return shdrs; 588 | } 589 | }, { 590 | key: "findTextures", 591 | value: function findTextures() { 592 | var textures = []; 593 | 594 | for (var c = 0; c < this.children.length; c++) { 595 | if (this.children[c] instanceof Texture) { 596 | textures.push(this.children[c]); 597 | } 598 | } 599 | 600 | return textures; 601 | } 602 | }, { 603 | key: "init", 604 | value: function init() { 605 | var _this3 = this; 606 | 607 | var shaders = this.findShaders(); 608 | this.useST = this.hasAttribute('shadertoy'); 609 | var fs = shaders.fragmentShader; 610 | var vs = shaders.vertexShader ? shaders.vertexShader : Template.defaultVertexShader(); 611 | this.uniforms = { 612 | resolution: { 613 | name: this.useST ? 'iResolution' : 'u_resolution', 614 | type: 'vec2', 615 | value: [0, 0] 616 | }, 617 | time: { 618 | name: this.useST ? 'iTime' : 'u_time', 619 | type: 'float', 620 | value: 0 621 | }, 622 | delta: { 623 | name: this.useST ? 'iTimeDelta' : 'u_delta', 624 | type: 'float', 625 | value: 0 626 | }, 627 | date: { 628 | name: this.useST ? 'iDate' : 'u_date', 629 | type: 'vec4', 630 | value: [0, 0, 0, 0] 631 | }, 632 | frame: { 633 | name: this.useST ? 'iFrame' : 'u_frame', 634 | type: 'int', 635 | value: 0 636 | }, 637 | mouse: { 638 | name: this.useST ? 'iMouse' : 'u_mouse', 639 | type: this.useST ? 'vec4' : 'vec2', 640 | value: this.useST ? [0, 0, 0, 0] : [0, 0] 641 | } 642 | }; 643 | this.shadow.innerHTML = Template.render(); 644 | this.canvas = Template.map(this.shadow).canvas; 645 | var gl = this.gl = this.canvas.getContext('webgl'); 646 | this.updateRect(); // format/replace special shadertoy io 647 | 648 | if (this.useST) { 649 | var io = fs.match(SHADERTOY_IO); 650 | fs = fs.replace('mainImage', 'main'); 651 | fs = fs.replace(SHADERTOY_IO, '()'); 652 | fs = (io ? "#define ".concat(io[1], " gl_FragColor\n#define ").concat(io[2], " gl_FragCoord.xy\n") : '') + fs; 653 | } 654 | 655 | var uniformString = Object.values(this.uniforms).reduce(function (acc, uniform) { 656 | return acc + "uniform ".concat(uniform.type, " ").concat(uniform.name, ";\n"); 657 | }, ''); 658 | fs = uniformString + fs; 659 | fs = 'precision highp float;\n' + fs; 660 | gl.clearColor(0, 0, 0, 0); 661 | this.vertexShader = this.makeShader(gl.VERTEX_SHADER, vs); 662 | this.fragmentShader = this.makeShader(gl.FRAGMENT_SHADER, fs); 663 | this.program = this.makeProgram(this.vertexShader, this.fragmentShader); // prettier-ignore 664 | 665 | this.vertices = new Float32Array([-1, 1, 1, 1, 1, -1, -1, 1, 1, -1, -1, -1]); 666 | this.buffer = gl.createBuffer(); 667 | gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); 668 | gl.bufferData(gl.ARRAY_BUFFER, this.vertices, gl.STATIC_DRAW); 669 | gl.useProgram(this.program); 670 | this.program.position = gl.getAttribLocation(this.program, 'position'); 671 | this.textures = this.findTextures(); 672 | this.textures.forEach(function (t, i) { 673 | // set texture name to 'u_texture_XX' if no name set 674 | if (!t.name) { 675 | t.name = "".concat(UNNAMED_TEXTURE_PREFIX).concat(_this3.unnamedTextureIndex++); 676 | } 677 | 678 | t.init(gl, _this3.program, i); 679 | }); 680 | gl.enableVertexAttribArray(this.program.position); 681 | gl.vertexAttribPointer(this.program.position, 2, gl.FLOAT, false, 0, 0); // get all uniform locations from shaders 682 | 683 | Object.values(this.uniforms).forEach(function (uniform) { 684 | uniform.location = gl.getUniformLocation(_this3.program, uniform.name); 685 | }); 686 | 687 | this._bind('mouseDown', 'mouseMove', 'mouseUp', 'render'); 688 | 689 | this.canvas.addEventListener('mousedown', this.mouseDown); 690 | this.canvas.addEventListener('mousemove', this.mouseMove); 691 | this.canvas.addEventListener('mouseup', this.mouseUp); 692 | this.render(); 693 | } 694 | }, { 695 | key: "render", 696 | value: function render(timestamp) { 697 | if (!this || !this.mounted || !this.gl) return; 698 | var gl = this.gl; 699 | this.textures.forEach(function (t) { 700 | t.update(); 701 | }); 702 | this.updateTimeUniforms(timestamp); 703 | this.updateRect(); 704 | gl.clear(gl.COLOR_BUFFER_BIT); 705 | Object.values(this.uniforms).forEach(function (_ref) { 706 | var type = _ref.type, 707 | location = _ref.location, 708 | value = _ref.value; 709 | var method = type.match(/vec/) ? "".concat(type[type.length - 1], "fv") : "1".concat(type[0]); 710 | gl["uniform".concat(method)](location, value); 711 | }); 712 | gl.drawArrays(gl.TRIANGLES, 0, this.vertices.length / 2); 713 | this.ticking = false; 714 | this.animationFrame = requestAnimationFrame(this.render); 715 | } 716 | }, { 717 | key: "mouseDown", 718 | value: function mouseDown(e) { 719 | if (this.useST) { 720 | this.mousedown = true; 721 | var _this$rect = this.rect, 722 | top = _this$rect.top, 723 | left = _this$rect.left, 724 | height = _this$rect.height; 725 | this.uniforms.mouse.value[2] = e.clientX - Math.floor(left); 726 | this.uniforms.mouse.value[3] = Math.floor(height) - (e.clientY - Math.floor(top)); 727 | } 728 | } 729 | }, { 730 | key: "mouseMove", 731 | value: function mouseMove(e) { 732 | if (!this.ticking && (!this.useST || this.mousedown)) { 733 | var _this$rect2 = this.rect, 734 | top = _this$rect2.top, 735 | left = _this$rect2.left, 736 | height = _this$rect2.height; 737 | this.uniforms.mouse.value[0] = e.clientX - Math.floor(left); 738 | this.uniforms.mouse.value[1] = Math.floor(height) - (e.clientY - Math.floor(top)); 739 | this.ticking = true; 740 | } 741 | } 742 | }, { 743 | key: "mouseUp", 744 | value: function mouseUp(e) { 745 | if (this.useST) { 746 | this.mousedown = false; 747 | this.uniforms.mouse.value[2] = 0; 748 | this.uniforms.mouse.value[3] = 0; 749 | } 750 | } 751 | }, { 752 | key: "updateTimeUniforms", 753 | value: function updateTimeUniforms(timestamp) { 754 | var delta = this.lastTime ? (timestamp - this.lastTime) / 1000 : 0; 755 | this.lastTime = timestamp; 756 | this.uniforms.time.value += delta; 757 | this.uniforms.delta.value = delta; 758 | this.uniforms.frame.value++; 759 | var d = new Date(); 760 | this.uniforms.date.value[0] = d.getFullYear(); 761 | this.uniforms.date.value[1] = d.getMonth() + 1; 762 | this.uniforms.date.value[2] = d.getDate(); 763 | this.uniforms.date.value[3] = d.getHours() * 60 * 60 + d.getMinutes() * 60 + d.getSeconds() + d.getMilliseconds() * 0.001; 764 | } 765 | }, { 766 | key: "updateRect", 767 | value: function updateRect() { 768 | this.rect = this.canvas.getBoundingClientRect(); 769 | var _this$rect3 = this.rect, 770 | width = _this$rect3.width, 771 | height = _this$rect3.height; 772 | var widthChanged = this.canvas.width !== width; 773 | var heightChanged = this.canvas.height !== height; 774 | 775 | if (widthChanged) { 776 | this.canvas.width = this.uniforms.resolution.value[0] = width; 777 | } 778 | 779 | if (heightChanged) { 780 | this.canvas.height = this.uniforms.resolution.value[1] = height; 781 | } 782 | 783 | if (widthChanged || heightChanged) { 784 | this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); 785 | } 786 | } 787 | }, { 788 | key: "makeShader", 789 | value: function makeShader(type, string) { 790 | var gl = this.gl; 791 | var shader = gl.createShader(type); 792 | gl.shaderSource(shader, string); 793 | gl.compileShader(shader); 794 | 795 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 796 | var compilationLog = gl.getShaderInfoLog(shader); 797 | gl.deleteShader(shader); 798 | console.warn(compilationLog, '\nin shader:\n', string); 799 | } 800 | 801 | return shader; 802 | } 803 | }, { 804 | key: "makeProgram", 805 | value: function makeProgram() { 806 | var gl = this.gl; 807 | var program = gl.createProgram(); 808 | 809 | for (var _len = arguments.length, shaders = new Array(_len), _key = 0; _key < _len; _key++) { 810 | shaders[_key] = arguments[_key]; 811 | } 812 | 813 | shaders.forEach(function (shader) { 814 | gl.attachShader(program, shader); 815 | }); 816 | gl.linkProgram(program); 817 | 818 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 819 | var linkLog = gl.getProgramInfoLog(this.program); 820 | console.warn(linkLog); 821 | } 822 | 823 | return program; 824 | } 825 | }, { 826 | key: "_bind", 827 | value: function _bind() { 828 | var _this4 = this; 829 | 830 | for (var _len2 = arguments.length, methods = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 831 | methods[_key2] = arguments[_key2]; 832 | } 833 | 834 | methods.forEach(function (method) { 835 | return _this4[method] = _this4[method].bind(_this4); 836 | }); 837 | } 838 | }]); 839 | 840 | return ShaderDoodle; 841 | }(_wrapNativeSuper(HTMLElement)); 842 | 843 | if (!customElements.get('shader-doodle')) { 844 | customElements.define('shader-doodle', ShaderDoodle); 845 | } 846 | -------------------------------------------------------------------------------- /docker-compose-traefik-v1.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | reverse-proxy: 4 | image: traefik:v1.7 # The official Traefik docker image 5 | command: --api --docker # Enables the web UI and tells Traefik to listen to docker 6 | ports: 7 | - "80:80" # The HTTP port 8 | - "8888:8080" # The Web UI (enabled by --api) 9 | volumes: 10 | - /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events 11 | 12 | nginx: 13 | image: nginx 14 | volumes: 15 | - ./demo:/usr/share/nginx/html:ro 16 | labels: 17 | - "traefik.enable=true" # Enable reverse-proxy for this service 18 | - "traefik.frontend.rule=Host:demo.haxcms.localhost" 19 | - "traefik.frontend.auth.forward.address=http://auth.haxcms.localhost/auth" 20 | - traefik.frontend.auth.forward.authResponseHeaders=X-Forwarded-User 21 | - traefik.frontend.auth.forward.trustForwardHeader=true 22 | - "traefik.port=80" 23 | 24 | server: 25 | build: . 26 | volumes: 27 | - /home/node/app/node_modules 28 | - .:/home/node/app 29 | labels: 30 | - "traefik.enable=true" # Enable reverse-proxy for this service 31 | - "traefik.frontend.rule=Host:auth.haxcms.localhost" 32 | - "traefik.port=4000" 33 | - "traefik.frontend.passHostHeader=true" 34 | environment: 35 | - PRISMA_DB_PROVIDER=postgresql 36 | - PRISMA_DB_URL=postgresql://prisma:prisma@postgres/ 37 | - GITHUB_SCOPE=user:email,read:user,public_rep 38 | depends_on: 39 | - postgres 40 | 41 | postgres: 42 | image: postgres:10.3 43 | restart: always 44 | environment: 45 | POSTGRES_USER: prisma 46 | POSTGRES_PASSWORD: prisma 47 | volumes: 48 | - postgres:/var/lib/postgresql/data 49 | labels: 50 | - "traefik.enable=true" # Enable reverse-proxy for this service 51 | - "traefik.port=5432" 52 | 53 | volumes: 54 | postgres: -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | reverse-proxy: 4 | image: traefik:2.1 # The official Traefik docker image 5 | command: 6 | - --api.insecure=true 7 | - --providers.docker=true 8 | - --providers.docker.exposedbydefault=false 9 | - --entrypoints.web.address=:80 10 | ports: 11 | - "80:80" # The HTTP port 12 | - "8888:8080" # The Web UI (enabled by --api) 13 | volumes: 14 | - /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events 15 | 16 | demo: 17 | image: nginx 18 | volumes: 19 | - ./demo:/usr/share/nginx/html:ro 20 | labels: 21 | - "traefik.enable=true" 22 | - "traefik.http.routers.demo.rule=Host(`demo.${SCOPE}`)" 23 | - "traefik.http.services.demo.loadbalancer.server.port=80" 24 | - "traefik.http.services.demo.loadbalancer.passhostheader=false" 25 | - "traefik.http.middlewares.demo.forwardauth.address=http://demo.${SCOPE}/auth" 26 | - "traefik.http.middlewares.demo.forwardauth.authResponseHeaders=X-Forwarded-User" 27 | - "traefik.http.middlewares.demo.forwardauth.trustForwardHeader=true" 28 | 29 | server: 30 | build: . 31 | volumes: 32 | - /home/node/app/node_modules 33 | - .:/home/node/app 34 | labels: 35 | - "traefik.enable=true" 36 | - "traefik.http.routers.auth.rule=Host(`auth.${SCOPE}`)" 37 | - "traefik.http.services.auth.loadbalancer.server.port=4000" 38 | - "traefik.http.services.auth.loadbalancer.passhostheader=false" 39 | environment: 40 | - PRISMA_DB_PROVIDER=postgresql 41 | - PRISMA_DB_URL=postgresql://prisma:prisma@postgres/ 42 | - GITHUB_SCOPE=user:email,read:user,public_rep 43 | depends_on: 44 | - postgres 45 | 46 | postgres: 47 | image: postgres:10.3 48 | restart: always 49 | environment: 50 | POSTGRES_USER: prisma 51 | POSTGRES_PASSWORD: prisma 52 | volumes: 53 | - postgres:/var/lib/postgresql/data 54 | labels: 55 | - "traefik.enable=true" # Enable reverse-proxy for this service 56 | - "traefik.port=5432" 57 | 58 | volumes: 59 | postgres: -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "script", 3 | "license": "MIT", 4 | "dependencies": { 5 | "dotenv": "^8.1.0", 6 | "express": "^4.17.1", 7 | "cors": "^2.8.5", 8 | "reflect-metadata": "0.1.13", 9 | "nodemon": "^1.19.2", 10 | "body-parser": "^1.19.0", 11 | "cookie-parser": "^1.4.4", 12 | "node-fetch": "^2.6.1", 13 | "jsonwebtoken": "^8.5.1", 14 | "apollo-server-express": "^2.14.2", 15 | "gql": "^1.1.2", 16 | "graphql": "^14.5.7" 17 | }, 18 | "devDependencies": { 19 | "ts-node": "8.4.1", 20 | "typescript": "3.6.3", 21 | "prisma2": "2.0.0-preview-12" 22 | }, 23 | "scripts": { 24 | "start": "yarn run migrate && yarn run generate && nodemon ./server/index.js", 25 | "seed": "ts-node ./seed.ts", 26 | "migrate": "prisma2 lift up -c", 27 | "generate": "prisma2 generate" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /prisma/migrations/20191002140626-initial/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20191002140626-initial` 2 | 3 | This migration has been generated at 10/2/2019, 2:06:26 PM. 4 | You can check out the [state of the datamodel](./datamodel.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | CREATE TABLE "public"."User" ( 10 | "createdAt" timestamp(3) NOT NULL DEFAULT '1970-01-01 00:00:00' , 11 | "email" text , 12 | "githubAccessToken" text , 13 | "id" text NOT NULL , 14 | "name" text NOT NULL DEFAULT '' , 15 | "updatedAt" timestamp(3) NOT NULL DEFAULT '1970-01-01 00:00:00' , 16 | PRIMARY KEY ("id") 17 | ); 18 | 19 | CREATE UNIQUE INDEX "User.name" ON "public"."User"("name") 20 | ``` 21 | 22 | ## Changes 23 | 24 | ```diff 25 | diff --git datamodel.mdl datamodel.mdl 26 | migration ..20191002140626-initial 27 | --- datamodel.dml 28 | +++ datamodel.dml 29 | @@ -1,0 +1,17 @@ 30 | +datasource db { 31 | + provider = env("PRISMA_DB_PROVIDER") 32 | + url = env("PRISMA_DB_URL") 33 | +} 34 | + 35 | +generator photon { 36 | + provider = "photonjs" 37 | +} 38 | + 39 | +model User { 40 | + id String @default(cuid()) @id 41 | + createdAt DateTime @default(now()) 42 | + updatedAt DateTime @updatedAt 43 | + name String @unique 44 | + email String? 45 | + githubAccessToken String? 46 | +} 47 | ``` 48 | 49 | ## Photon Usage 50 | 51 | You can use a specific Photon built for this migration (20191002140626-initial) 52 | in your `before` or `after` migration script like this: 53 | 54 | ```ts 55 | import Photon from '@generated/photon/20191002140626-initial' 56 | 57 | const photon = new Photon() 58 | 59 | async function main() { 60 | const result = await photon.users() 61 | console.dir(result, { depth: null }) 62 | } 63 | 64 | main() 65 | 66 | ``` 67 | -------------------------------------------------------------------------------- /prisma/migrations/20191002140626-initial/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = env("PRISMA_DB_PROVIDER") 3 | url = env("PRISMA_DB_URL") 4 | } 5 | 6 | generator photon { 7 | provider = "photonjs" 8 | } 9 | 10 | model User { 11 | id String @default(cuid()) @id 12 | createdAt DateTime @default(now()) 13 | updatedAt DateTime @updatedAt 14 | name String @unique 15 | email String? 16 | githubAccessToken String? 17 | } -------------------------------------------------------------------------------- /prisma/migrations/20191002140626-initial/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.55", 3 | "steps": [ 4 | { 5 | "stepType": "CreateModel", 6 | "name": "User", 7 | "embedded": false 8 | }, 9 | { 10 | "stepType": "CreateField", 11 | "model": "User", 12 | "name": "id", 13 | "type": { 14 | "Base": "String" 15 | }, 16 | "arity": "required", 17 | "isUnique": false, 18 | "id": { 19 | "strategy": "Auto", 20 | "sequence": null 21 | }, 22 | "default": { 23 | "Expression": [ 24 | "cuid", 25 | "String", 26 | [] 27 | ] 28 | } 29 | }, 30 | { 31 | "stepType": "CreateField", 32 | "model": "User", 33 | "name": "createdAt", 34 | "type": { 35 | "Base": "DateTime" 36 | }, 37 | "arity": "required", 38 | "isUnique": false, 39 | "default": { 40 | "Expression": [ 41 | "now", 42 | "DateTime", 43 | [] 44 | ] 45 | } 46 | }, 47 | { 48 | "stepType": "CreateField", 49 | "model": "User", 50 | "name": "updatedAt", 51 | "type": { 52 | "Base": "DateTime" 53 | }, 54 | "arity": "required", 55 | "isUnique": false 56 | }, 57 | { 58 | "stepType": "CreateField", 59 | "model": "User", 60 | "name": "name", 61 | "type": { 62 | "Base": "String" 63 | }, 64 | "arity": "required", 65 | "isUnique": true 66 | }, 67 | { 68 | "stepType": "CreateField", 69 | "model": "User", 70 | "name": "email", 71 | "type": { 72 | "Base": "String" 73 | }, 74 | "arity": "optional", 75 | "isUnique": false 76 | }, 77 | { 78 | "stepType": "CreateField", 79 | "model": "User", 80 | "name": "githubAccessToken", 81 | "type": { 82 | "Base": "String" 83 | }, 84 | "arity": "optional", 85 | "isUnique": false 86 | } 87 | ] 88 | } -------------------------------------------------------------------------------- /prisma/migrations/lift.lock: -------------------------------------------------------------------------------- 1 | # IF THERE'S A GIT CONFLICT IN THIS FILE, DON'T SOLVE IT MANUALLY! 2 | # INSTEAD EXECUTE `prisma lift fix` 3 | # lift lockfile v1 4 | # Read more about conflict resolution here: TODO 5 | 6 | 20191002140626-initial -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = env("PRISMA_DB_PROVIDER") 3 | url = env("PRISMA_DB_URL") 4 | } 5 | 6 | generator photon { 7 | provider = "photonjs" 8 | } 9 | 10 | model User { 11 | id String @default(cuid()) @id 12 | createdAt DateTime @default(now()) 13 | updatedAt DateTime @updatedAt 14 | name String @unique 15 | email String? 16 | githubAccessToken String? 17 | } -------------------------------------------------------------------------------- /seed.ts: -------------------------------------------------------------------------------- 1 | import { Photon } from '@generated/photon' 2 | 3 | const photon = new Photon() 4 | 5 | // A `main` function so that we can use async/await 6 | async function main() { 7 | // const users = await photon.users 8 | // .findOne({ 9 | // where: { 10 | // name: 'heyMP' 11 | // } 12 | // }) 13 | // console.log(users) 14 | // Seed the database with users and posts 15 | const user1 = await photon.users.create({ 16 | data: { 17 | name: 'heyMP', 18 | } 19 | }) 20 | const user2 = await photon.users.create({ 21 | data: { 22 | name: 'btopro', 23 | } 24 | }) 25 | } 26 | // console.log(`Created users: ${user1.name} (${user1.posts.length} post) and (${user2.posts.length} posts) `) 27 | 28 | // // Retrieve all published posts 29 | // const allPosts = await photon.posts.findMany({ 30 | // where: { published: true }, 31 | // }) 32 | // console.log(`Retrieved all published posts: `, allPosts) 33 | 34 | // // Create a new post (written by an already existing user with email alice@prisma.io) 35 | // const newPost = await photon.posts.create({ 36 | // data: { 37 | // title: 'Join the Prisma Slack community', 38 | // content: 'http://slack.prisma.io', 39 | // published: false, 40 | // author: { 41 | // connect: { 42 | // email: 'alice@prisma.io', 43 | // }, 44 | // }, 45 | // }, 46 | // }) 47 | // console.log(`Created a new post: `, newPost) 48 | 49 | // // Publish the new post 50 | // const updatedPost = await photon.posts.update({ 51 | // where: { 52 | // id: newPost.id, 53 | // }, 54 | // data: { 55 | // published: true, 56 | // }, 57 | // }) 58 | // console.log(`Published the newly created post: `, updatedPost) 59 | 60 | // // Retrieve all posts by user with email alice@prisma.io 61 | // const postsByUser = await photon.users 62 | // .findOne({ 63 | // where: { 64 | // email: 'alice@prisma.io', 65 | // }, 66 | // }) 67 | // .posts() 68 | // console.log(`Retrieved all posts from a specific user: `, postsByUser) 69 | // } 70 | 71 | main() 72 | .catch(e => console.error(e)) 73 | .finally(async () => { 74 | await photon.disconnect() 75 | }) 76 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const express = require("express"); 3 | const cors = require("cors"); 4 | const bodyParser = require("body-parser"); 5 | const cookieParser = require("cookie-parser"); 6 | const fetch = require("node-fetch"); 7 | const { Photon } = require("@generated/photon"); 8 | const photon = new Photon(); 9 | const jwt = require("jsonwebtoken"); 10 | const { ApolloServer, gql, AuthenticationError } = require("apollo-server-express"); 11 | 12 | const CLIENT_ID = process.env.GITHUB_CLIENT_ID; 13 | const CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET; 14 | const GITHUB_SCOPE = process.env.GITHUB_SCOPE; 15 | const HAXCMS_OAUTH_JWT_SECRET = process.env.HAXCMS_OAUTH_JWT_SECRET; 16 | const HAXCMS_OAUTH_JWT_REFRESH_SECRET = 17 | process.env.HAXCMS_OAUTH_JWT_REFRESH_SECRET; 18 | const FQDN = process.env.FQDN; 19 | const SCOPE = process.env.SCOPE; 20 | 21 | async function main() { 22 | await photon.connect(); 23 | const app = express(); 24 | 25 | const getUserFromAuthHeader = async req => { 26 | try { 27 | if (typeof req.headers.authorization !== "undefined") { 28 | const access_token = req.headers.authorization.split(" ")[1]; 29 | const user = jwt.verify(access_token, HAXCMS_OAUTH_JWT_SECRET); 30 | return user; 31 | } 32 | } catch (error) { 33 | throw new AuthenticationError(error); 34 | return null; 35 | } 36 | }; 37 | 38 | app.use( 39 | cors({ 40 | credentials: true 41 | }) 42 | ); 43 | app.use(bodyParser.json()); 44 | app.use(cookieParser()); 45 | 46 | /** 47 | * Allow calls from web components with cookies 48 | */ 49 | app.use((req, res, next) => { 50 | res.header("Access-Control-Allow-Origin", req.headers.origin); 51 | next(); 52 | }); 53 | 54 | // Make sure that there isn't a scenario where the user is logged in but they 55 | // don't exist in the database 56 | app.use(async (req, res, next) => { 57 | try { 58 | const { access_token } = req.cookies; 59 | if (access_token) { 60 | const { name } = jwt.verify(access_token, HAXCMS_OAUTH_JWT_SECRET); 61 | await photon.users.findOne({ where: { name } }); 62 | } 63 | } catch (error) { 64 | delete req.cookies.access_token; 65 | res.clearCookie("access_token", { domain: SCOPE }); 66 | } 67 | next(); 68 | }); 69 | 70 | app.get("/auth", async (req, res) => { 71 | // Decode jwt 72 | try { 73 | const { refresh_token } = req.cookies; 74 | const { name } = jwt.verify( 75 | refresh_token, 76 | HAXCMS_OAUTH_JWT_REFRESH_SECRET 77 | ); 78 | res.status(200); 79 | res.send("OK"); 80 | } catch (error) { 81 | const redirect = 82 | typeof req.headers["x-forwarded-host"] !== "undefined" 83 | ? `redirect=${req.headers["x-forwarded-proto"]}://${ 84 | req.headers["x-forwarded-host"] 85 | }` 86 | : ``; 87 | res.redirect(`/login/?${redirect}`); 88 | } 89 | }); 90 | 91 | app.get("/access_token", async (req, res, next) => { 92 | try { 93 | const { refresh_token } = req.cookies; 94 | if (refresh_token) { 95 | const { name } = jwt.verify( 96 | refresh_token, 97 | HAXCMS_OAUTH_JWT_REFRESH_SECRET 98 | ); 99 | const jwtToken = await jwt.sign({ name }, HAXCMS_OAUTH_JWT_SECRET, { expiresIn: 60 * 15 }) 100 | res.json(jwtToken); 101 | } 102 | else { 103 | throw new AuthenticationError('No refresh token found.') 104 | } 105 | } catch (error) { 106 | next(new AuthenticationError(error)) 107 | } 108 | }); 109 | 110 | app.get("/logout", (req, res) => { 111 | // When deleting a cookie you need to also include the path and domain 112 | res.clearCookie("refresh_token", { domain: SCOPE }); 113 | res.redirect("/"); 114 | }); 115 | 116 | app.get("/login", (req, res) => { 117 | // get a redirect query parameter 118 | const redirect = 119 | // if we have a redirect query then use it 120 | typeof req.query.redirect !== "undefined" 121 | ? `redirect=${req.query.redirect}` 122 | : // else if there is an x-forwared-host defined then use that 123 | typeof req.headers["x-forwarded-host"] !== "undefined" 124 | ? `redirect=${req.headers["x-forwarded-proto"]}://${ 125 | req.headers["x-forwarded-host"] 126 | }` 127 | : // else just redirect to the home page. 128 | `redirect=${FQDN}/auth`; 129 | 130 | res.redirect( 131 | `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&scope=${GITHUB_SCOPE}&redirect_uri=${FQDN}/login/callback?${redirect}` 132 | ); 133 | }); 134 | 135 | app.get("/login/callback", async (req, res, next) => { 136 | const { code } = req.query; 137 | 138 | try { 139 | // get the access token 140 | const { access_token } = await fetch( 141 | "https://github.com/login/oauth/access_token", 142 | { 143 | method: "POST", 144 | headers: { 145 | Accept: "application/json", 146 | "Content-Type": "application/json" 147 | }, 148 | body: JSON.stringify({ 149 | client_id: CLIENT_ID, 150 | client_secret: CLIENT_SECRET, 151 | code 152 | }) 153 | } 154 | ).then(_res => _res.json()); 155 | 156 | // get the username from github 157 | const userFetch = await fetch("https://api.github.com/graphql", { 158 | method: "POST", 159 | headers: { 160 | Authorization: `bearer ${access_token}`, 161 | Accept: "application/json", 162 | "Content-Type": "application/json" 163 | }, 164 | body: 165 | ' \ 166 | { \ 167 | "query": "query { viewer { login email }}" \ 168 | } \ 169 | ' 170 | }).then(_res => _res.json()); 171 | 172 | const userName = userFetch.data.viewer.login; 173 | const user = await photon.users.upsert({ 174 | where: { name: userName }, 175 | update: { }, 176 | create: { 177 | name: userName 178 | } 179 | }); 180 | 181 | // Create JWT for the user 182 | const refreshJwtToken = await jwt.sign( 183 | { name: user.name }, 184 | HAXCMS_OAUTH_JWT_REFRESH_SECRET, 185 | { 186 | expiresIn: "7d" 187 | } 188 | ); 189 | 190 | // if the user specified a redirect url then redirect with a cookie 191 | res.cookie("refresh_token", refreshJwtToken, { 192 | httpOnly: true, 193 | domain: SCOPE 194 | }); 195 | 196 | res.redirect(req.query.redirect); 197 | } catch (error) { 198 | next(error); 199 | } 200 | }); 201 | 202 | const apolloServer = new ApolloServer({ 203 | typeDefs: gql` 204 | type User { 205 | id: String 206 | name: String 207 | githubAccessToken: String 208 | email: String 209 | } 210 | 211 | type Query { 212 | users: [User] 213 | user: User 214 | } 215 | `, 216 | resolvers: { 217 | Query: { 218 | users: async () => await photon.users(), 219 | user: async (parent, _, ctx) => { 220 | const { name } = await ctx.user 221 | return await photon.users.findOne({ where: { name } }) 222 | } 223 | } 224 | }, 225 | context: ({ req }) => ({ 226 | user: getUserFromAuthHeader(req) 227 | }) 228 | }); 229 | 230 | apolloServer.applyMiddleware({ app }); 231 | 232 | app.listen(4000, () => { 233 | console.log( 234 | `🚀 Server ready at http://localhost:4000${apolloServer.graphqlPath}` 235 | ); 236 | }); 237 | } 238 | 239 | main() 240 | .catch(e => { 241 | console.error(e); 242 | }) 243 | .finally(async () => {}); 244 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "outDir": "dist", 5 | "strict": true, 6 | "lib": ["esnext", "dom"], 7 | "esModuleInterop": true 8 | } 9 | } 10 | --------------------------------------------------------------------------------