├── .dockerignore
├── .gitignore
├── demo
├── scripts.js
├── web_modules
│ ├── import-map.json
│ ├── @adobe
│ │ └── lit-mobx.js
│ ├── @lrnwebcomponents
│ │ └── hax-logo.js
│ ├── shader-doodle.js
│ └── @vaadin
│ │ └── router.js
├── package.json
├── hod-button.js
├── store.js
├── index.html
├── hod-auth.js
├── hod-toolbar.js
├── hod-create-container.js
└── package-lock.json
├── tsconfig.json
├── prisma
├── migrations
│ ├── lift.lock
│ └── 20191002140626-initial
│ │ ├── schema.prisma
│ │ ├── README.md
│ │ └── steps.json
└── schema.prisma
├── Dockerfile
├── .env.example
├── README.md
├── package.json
├── docker-compose-traefik-v1.yml
├── docker-compose.yml
├── seed.ts
└── server
└── index.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/demo/scripts.js:
--------------------------------------------------------------------------------
1 | import './hod-auth.js'
2 | import './hod-toolbar.js'
3 | import './hod-create-container.js'
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "outDir": "dist",
5 | "strict": true,
6 | "lib": ["esnext", "dom"],
7 | "esModuleInterop": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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" ]
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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()
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | HAXcms On Demand
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-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 |
54 |
55 |
56 |
57 |

58 |
${this.store.name}
59 |
60 | `
61 | }
62 | }
63 | customElements.define("hod-toolbar", HodToolbar);
64 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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:
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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:
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------