Repository: spring-petclinic/spring-petclinic-graphql Branch: main Commit: 0aa4212fb4b1 Files: 349 Total size: 3.1 MB Directory structure: gitextract_pbnw42or/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── build-app.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .mvn/ │ └── wrapper/ │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .run/ │ └── Frontend.run.xml ├── LICENCE ├── backend/ │ ├── .editorconfig │ ├── .gitignore │ ├── pom.xml │ ├── sample-queries.graphql │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── springframework/ │ │ │ └── samples/ │ │ │ └── petclinic/ │ │ │ ├── FakeDataSqlCreator.java │ │ │ ├── PetClinicApplication.java │ │ │ ├── auth/ │ │ │ │ ├── Role.java │ │ │ │ ├── User.java │ │ │ │ └── UserRepository.java │ │ │ ├── graphql/ │ │ │ │ ├── AbstractOwnerInput.java │ │ │ │ ├── AbstractOwnerPayload.java │ │ │ │ ├── AbstractPetInput.java │ │ │ │ ├── AbstractPetPayload.java │ │ │ │ ├── AddOwnerInput.java │ │ │ │ ├── AddOwnerPayload.java │ │ │ │ ├── AddPetInput.java │ │ │ │ ├── AddPetPayload.java │ │ │ │ ├── AddSpecialtyPayload.java │ │ │ │ ├── AddVetErrorPayload.java │ │ │ │ ├── AddVetInput.java │ │ │ │ ├── AddVetPayload.java │ │ │ │ ├── AddVetSuccessPayload.java │ │ │ │ ├── AddVisitInput.java │ │ │ │ ├── AddVisitPayload.java │ │ │ │ ├── AuthController.java │ │ │ │ ├── OwnerController.java │ │ │ │ ├── PageInfo.java │ │ │ │ ├── PetController.java │ │ │ │ ├── PetTypeController.java │ │ │ │ ├── RemoveSpecialtyPayload.java │ │ │ │ ├── SpecialtyController.java │ │ │ │ ├── UpdateOwnerInput.java │ │ │ │ ├── UpdateOwnerPayload.java │ │ │ │ ├── UpdatePetInput.java │ │ │ │ ├── UpdatePetPayload.java │ │ │ │ ├── UpdateSpecialtyInput.java │ │ │ │ ├── UpdateSpecialtyPayload.java │ │ │ │ ├── VetController.java │ │ │ │ ├── VisitConnection.java │ │ │ │ ├── VisitController.java │ │ │ │ ├── VisitPublisher.java │ │ │ │ └── runtime/ │ │ │ │ ├── DateCoercing.java │ │ │ │ ├── GraphiQlConfiguration.java │ │ │ │ └── PetClinicRuntimeWiringConfiguration.java │ │ │ ├── model/ │ │ │ │ ├── BaseEntity.java │ │ │ │ ├── InvalidVetDataException.java │ │ │ │ ├── NamedEntity.java │ │ │ │ ├── OrderField.java │ │ │ │ ├── Owner.java │ │ │ │ ├── OwnerFilter.java │ │ │ │ ├── OwnerOrder.java │ │ │ │ ├── OwnerService.java │ │ │ │ ├── Person.java │ │ │ │ ├── Pet.java │ │ │ │ ├── PetService.java │ │ │ │ ├── PetType.java │ │ │ │ ├── PetValidator.java │ │ │ │ ├── Specialty.java │ │ │ │ ├── SpecialtyService.java │ │ │ │ ├── Vet.java │ │ │ │ ├── VetService.java │ │ │ │ ├── Vets.java │ │ │ │ ├── Visit.java │ │ │ │ ├── VisitCreatedEvent.java │ │ │ │ ├── VisitService.java │ │ │ │ └── package-info.java │ │ │ ├── repository/ │ │ │ │ ├── OwnerRepository.java │ │ │ │ ├── PetRepository.java │ │ │ │ ├── PetTypeRepository.java │ │ │ │ ├── SpecialtyRepository.java │ │ │ │ ├── VetRepository.java │ │ │ │ └── VisitRepository.java │ │ │ ├── security/ │ │ │ │ ├── JwtTokenService.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── LoginRequest.java │ │ │ │ ├── LoginResponse.java │ │ │ │ ├── NeverExpiringTokenGenerator.java │ │ │ │ ├── RSAKeyProvider.java │ │ │ │ └── SecurityConfig.java │ │ │ └── util/ │ │ │ └── EntityUtils.java │ │ └── resources/ │ │ ├── application.properties │ │ ├── db/ │ │ │ └── migration/ │ │ │ ├── V100_1__create_schema.sql │ │ │ └── V100_2__fill_db.sql │ │ ├── graphql/ │ │ │ └── petclinic.graphqls │ │ ├── keys/ │ │ │ ├── private_key.pem │ │ │ └── public_key.pem │ │ ├── readme-graphiql.md │ │ ├── testdata/ │ │ │ ├── owners.csv │ │ │ ├── pets.csv │ │ │ └── visits.csv │ │ └── ui/ │ │ └── graphiql/ │ │ ├── assets/ │ │ │ ├── Range-52ddcb6a.js │ │ │ ├── SchemaReference.es-0ccab37b.js │ │ │ ├── brace-fold.es-f2e3735d.js │ │ │ ├── closebrackets.es-e969742b.js │ │ │ ├── codemirror.es-52e8b92d.js │ │ │ ├── codemirror.es2-5884f31a.js │ │ │ ├── comment.es-39699bae.js │ │ │ ├── dialog.es-b2776d29.js │ │ │ ├── foldgutter.es-b6cee46a.js │ │ │ ├── forEachState.es-b2033c2b.js │ │ │ ├── hint.es-1418191b.js │ │ │ ├── hint.es2-598d3bfe.js │ │ │ ├── index-27dc12ba.js │ │ │ ├── index-928ba5be.css │ │ │ ├── info-addon.es-c9b2027b.js │ │ │ ├── info.es-3175bfab.js │ │ │ ├── javascript.es-3c6957c5.js │ │ │ ├── jump-to-line.es-3afd5e0a.js │ │ │ ├── jump.es-7b275cf1.js │ │ │ ├── lint.es-fe7166bb.js │ │ │ ├── lint.es2-97c4a6f4.js │ │ │ ├── lint.es3-bcaf3718.js │ │ │ ├── matchbrackets.es-97d2e827.js │ │ │ ├── matchbrackets.es2-f53f57e6.js │ │ │ ├── mode-indent.es-057a4f6a.js │ │ │ ├── mode.es-8c5bcfbd.js │ │ │ ├── mode.es2-8a6e8f8c.js │ │ │ ├── mode.es3-fa110728.js │ │ │ ├── search.es-2e392dd0.js │ │ │ ├── searchcursor.es-b1a352a2.js │ │ │ ├── searchcursor.es2-cbfe7cae.js │ │ │ ├── show-hint.es-b981493e.js │ │ │ └── sublime.es-e2a3eb60.js │ │ └── index.html │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── samples/ │ │ └── petclinic/ │ │ ├── PetClinicTestDbConfiguration.java │ │ ├── graphql/ │ │ │ ├── AbstractClinicGraphqlTests.java │ │ │ ├── AuthControllerTests.java │ │ │ ├── GraphQlTokenProvider.java │ │ │ ├── OwnerControllerTests.java │ │ │ ├── PetControllerTests.java │ │ │ ├── PetTypeControllerTests.java │ │ │ ├── SpecialtyControllerTests.java │ │ │ ├── VetControllerTests.java │ │ │ ├── VisitControllerTests.java │ │ │ └── VisitSubscriptionTest.java │ │ ├── model/ │ │ │ └── ValidatorTests.java │ │ └── repository/ │ │ ├── ApplicationTestConfig.java │ │ └── ClinicRepositorySpringDataJpaTests.java │ └── resources/ │ └── graphql-test/ │ ├── addPetMutation.graphql │ ├── addVetMutation.graphql │ ├── addVisitMutation.graphql │ ├── addVisitMutationWithVariables.graphql │ ├── meQuery.graphql │ └── updatePetMutation.graphql ├── build-local.sh ├── docker-compose-petclinic.yml ├── docker-compose.yml ├── e2e-tests/ │ ├── .github/ │ │ └── workflows/ │ │ └── playwright.yml │ ├── .gitignore │ ├── .prettierrc │ ├── package.json │ ├── playwright.config.ts │ ├── pom.xml │ └── tests/ │ ├── graphiql.spec.ts │ ├── owner-detail.spec.ts │ ├── owner-search-page.spec.ts │ ├── petclinic.fixtures.ts │ └── vets.spec.ts ├── frontend/ │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierignore │ ├── Dockerfile │ ├── README.md │ ├── codegen.ts │ ├── docker/ │ │ └── nginx.conf │ ├── index.html │ ├── package.json │ ├── pom.xml │ ├── postcss.config.js │ ├── prettier.config.cjs │ ├── src/ │ │ ├── App.css │ │ ├── App.tsx │ │ ├── NotFoundPage.tsx │ │ ├── WelcomePage.tsx │ │ ├── assets/ │ │ │ └── readme.md │ │ ├── components/ │ │ │ ├── Button.tsx │ │ │ ├── ButtonBar.tsx │ │ │ ├── Card.tsx │ │ │ ├── Heading.tsx │ │ │ ├── Input.tsx │ │ │ ├── Label.tsx │ │ │ ├── Link.tsx │ │ │ ├── Nav.tsx │ │ │ ├── PageHeader.tsx │ │ │ ├── PageLayout.tsx │ │ │ ├── Section.tsx │ │ │ ├── Select.tsx │ │ │ └── Table.tsx │ │ ├── create-graphql-client.ts │ │ ├── fonts/ │ │ │ ├── README.txt │ │ │ ├── generator_config.txt │ │ │ ├── metropolis-bold-demo.html │ │ │ ├── metropolis-extrabold-demo.html │ │ │ ├── metropolis-regular-demo.html │ │ │ └── specimen_files/ │ │ │ ├── grid_12-825-55-15.css │ │ │ └── specimen_stylesheet.css │ │ ├── fonts.css │ │ ├── graphql-types.txt │ │ ├── index.css │ │ ├── login/ │ │ │ ├── AuthTokenProvider.tsx │ │ │ ├── LoginPage.tsx │ │ │ └── MeQuery.graphql │ │ ├── main.tsx │ │ ├── owners/ │ │ │ ├── AddVisit.graphql │ │ │ ├── AllVetNames.graphql │ │ │ ├── FindOwnerByLastName.graphql │ │ │ ├── FindOwnerWithPetsAndVisits.graphql │ │ │ ├── NewVisitForm.tsx │ │ │ ├── NewVisitPanel.tsx │ │ │ ├── OnNewVisit.graphql │ │ │ ├── OwnerFields.graphql │ │ │ ├── OwnerPage.tsx │ │ │ ├── OwnerSearchPage.tsx │ │ │ ├── Visit.fragment.graphql │ │ │ └── VisitWithVet.fragment.graphql │ │ ├── urls.ts │ │ ├── use-current-user-fullname.tsx │ │ ├── use-logout.ts │ │ ├── utils.ts │ │ ├── vets/ │ │ │ ├── AddVet.graphql │ │ │ ├── AddVetForm.tsx │ │ │ ├── AllSpecialties.graphql │ │ │ ├── AllVets.graphql │ │ │ ├── VetAndVisits.graphql │ │ │ ├── VetsOverview.tsx │ │ │ └── VetsPage.tsx │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── graphql.config.yml ├── login.http ├── mvnw ├── mvnw.cmd ├── petclinic-graphiql/ │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── pom.xml │ ├── src/ │ │ ├── App.tsx │ │ ├── LoginForm.tsx │ │ ├── PetClinicGraphiql.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── urls.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── pom.xml ├── readme.md └── talk/ ├── curl-demo.sh ├── graphql-introduction.html ├── lib/ │ ├── jquery-2.2.4.js │ └── js/ │ └── line-numbers.js └── reveal.js/ ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── css/ │ ├── print/ │ │ ├── paper.css │ │ └── pdf.css │ ├── reveal.css │ ├── reveal.scss │ └── theme/ │ ├── README.md │ ├── beige.css │ ├── black.css │ ├── blood.css │ ├── fonts/ │ │ └── google-fonts.css │ ├── league.css │ ├── moon.css │ ├── night.css │ ├── serif.css │ ├── simple.css │ ├── sky.css │ ├── solarized.css │ ├── source/ │ │ ├── beige.scss │ │ ├── black.scss │ │ ├── blood.scss │ │ ├── league.scss │ │ ├── moon.scss │ │ ├── night.scss │ │ ├── serif.scss │ │ ├── simple.scss │ │ ├── sky.scss │ │ ├── solarized.scss │ │ └── white.scss │ ├── template/ │ │ ├── mixins.scss │ │ ├── settings.scss │ │ └── theme.scss │ └── white.css ├── index.html ├── js/ │ └── reveal.js ├── lib/ │ ├── css/ │ │ └── zenburn.css │ ├── font/ │ │ ├── league-gothic/ │ │ │ ├── LICENSE │ │ │ └── league-gothic.css │ │ └── source-sans-pro/ │ │ ├── LICENSE │ │ └── source-sans-pro.css │ └── js/ │ ├── classList.js │ └── html5shiv.js ├── package.json ├── plugin/ │ ├── highlight/ │ │ └── highlight.js │ ├── markdown/ │ │ ├── example.html │ │ ├── example.md │ │ ├── markdown.js │ │ └── marked.js │ ├── math/ │ │ └── math.js │ ├── multiplex/ │ │ ├── client.js │ │ ├── index.js │ │ └── master.js │ ├── notes/ │ │ ├── notes.html │ │ └── notes.js │ ├── notes-server/ │ │ ├── client.js │ │ ├── index.js │ │ └── notes.html │ ├── print-pdf/ │ │ └── print-pdf.js │ ├── search/ │ │ └── search.js │ └── zoom-js/ │ └── zoom.js └── test/ ├── examples/ │ ├── barebones.html │ ├── embedded-media.html │ ├── math.html │ ├── slide-backgrounds.html │ └── slide-transitions.html ├── qunit-1.12.0.css ├── qunit-1.12.0.js ├── test-markdown-element-attributes.html ├── test-markdown-element-attributes.js ├── test-markdown-slide-attributes.html ├── test-markdown-slide-attributes.js ├── test-markdown.html ├── test-markdown.js ├── test-pdf.html ├── test-pdf.js ├── test.html └── test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.ai binary *.eps binary ================================================ FILE: .github/workflows/build-app.yml ================================================ on: - push jobs: petclinic-graphiql: runs-on: ubuntu-latest defaults: run: working-directory: ./petclinic-graphiql steps: - name: Checkout uses: actions/checkout@v3 - name: Install Node.js uses: actions/setup-node@v3 with: node-version: 18 - uses: pnpm/action-setup@v2.0.1 name: Install pnpm id: pnpm-install with: version: 8 run_install: false - name: Install dependencies run: pnpm install - name: Build application run: pnpm build - name: Archive artifacts uses: actions/upload-artifact@v3 with: name: graphiql-dist path: ./petclinic-graphiql/dist/** retention-days: 1 backend: runs-on: ubuntu-latest needs: petclinic-graphiql steps: - name: Checkout uses: actions/checkout@v3 - name: Install Java 21 uses: actions/setup-java@v3 with: java-version: "21" distribution: "temurin" cache: maven - name: Clear embedded graphiql run: rm -rf backend/src/main/resources/ui/graphiql - name: Download graphiql uses: actions/download-artifact@v3 with: name: graphiql-dist path: backend/src/main/resources/ui/graphiql - name: show graphiql artifact run: ls -lR backend/src/main/resources/ui/ - name: Build with maven run: ./mvnw -pl backend spring-boot:build-image -Dspring-boot.build-image.imageName=spring-petclinic/petclinic-graphql-backend:0.0.1 - name: Export docker image run: docker save spring-petclinic/petclinic-graphql-backend:0.0.1|gzip>petclinic-graphql-backend.tar.gz - name: Archive artifacts uses: actions/upload-artifact@v3 with: name: petclinic-backend-docker path: petclinic-graphql-backend.tar.gz retention-days: 1 frontend: runs-on: ubuntu-latest defaults: run: working-directory: ./frontend steps: - name: Checkout uses: actions/checkout@v3 - name: Install Node.js uses: actions/setup-node@v3 with: node-version: 18 - uses: pnpm/action-setup@v2.0.1 name: Install pnpm id: pnpm-install with: version: 8 run_install: false - name: Install dependencies run: pnpm install - name: Code checks run: pnpm run check - name: Build application run: pnpm build - name: Build frontend docker image run: docker build . --tag spring-petclinic/petclinic-graphql-frontend:0.0.1 - name: Export frontend docker image run: docker save spring-petclinic/petclinic-graphql-frontend:0.0.1|gzip>../petclinic-graphql-frontend.tar.gz - name: LS run: ls -lR .. - name: Archive artifacts uses: actions/upload-artifact@v3 with: name: petclinic-frontend-docker path: petclinic-graphql-frontend.tar.gz retention-days: 1 end-to-end-test: timeout-minutes: 60 runs-on: ubuntu-latest needs: [petclinic-graphiql,backend,frontend] steps: - name: setup playwright uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - name: Download backend artifacts uses: actions/download-artifact@v3 with: name: petclinic-backend-docker path: petclinic-backend-docker - name: Download frontend docker image uses: actions/download-artifact@v3 with: name: petclinic-frontend-docker path: petclinic-frontend-docker - name: LS run: ls -lR - name: Import backend Docker image run: gunzip -c petclinic-backend-docker/petclinic-graphql-backend.tar.gz|docker load - name: Import frontend Docker image run: gunzip -c petclinic-frontend-docker/petclinic-graphql-frontend.tar.gz|docker load - name: run docker compose run: docker-compose -f docker-compose-petclinic.yml up -d - uses: pnpm/action-setup@v2.0.1 name: Install pnpm id: pnpm-install with: version: 8 run_install: false - name: Install dependencies working-directory: ./e2e-tests run: pnpm install - name: Install Playwright Browsers working-directory: ./e2e-tests run: pnpm exec playwright install --with-deps - name: wait for backend on port 3090 uses: iFaxity/wait-on-action@v1.1.0 with: resource: http-get://localhost:3090 - name: wait for frontend on port 3091 uses: iFaxity/wait-on-action@v1.1.0 with: resource: http-get://localhost:3091 - name: Run Playwright tests working-directory: ./e2e-tests run: pnpm test:docker-compose - name: Test Report uses: dorny/test-reporter@v1 if: success() || failure() # run this step even if previous step failed with: name: PlayWright Test # Name of the check run which will be created path: e2e-tests/test-results.xml # Path to test results reporter: java-junit # Format of test results - uses: actions/upload-artifact@v3 if: always() with: name: playwright-report path: ./e2e-tests/playwright-report/ retention-days: 2 ================================================ FILE: .gitignore ================================================ target/* .settings/* .classpath .project .idea *.iml /target */target/ generated/ # Easier branch switching springboot-petclinic-client/ springboot-petclinic-server/ node_modules/ npm-debug.log dist/ frontend/.eslintcache ================================================ FILE: .gitpod.Dockerfile ================================================ FROM gitpod/workspace-full USER gitpod RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \ sdk install java 21-tem && \ sdk default java 21-tem" # Install custom tools, runtime, etc. using apt-get # For example, the command below would install "bastet" - a command line tetris clone: # # RUN sudo apt-get -q update && \ # sudo apt-get install -yq bastet && \ # sudo rm -rf /var/lib/apt/lists/* # # More information: https://www.gitpod.io/docs/config-docker/ ================================================ FILE: .gitpod.yml ================================================ image: file: .gitpod.Dockerfile tasks: - name: Build init: | ./mvnw package -Dmaven.test.skip=true -pl backend gp sync-done build - name: "Run Petclinic Backend" init: | gp sync-await build command: | export SPRING_DOCKER_COMPOSE_FILE=/workspace/spring-petclinic-graphql/docker-compose.yml ./mvnw spring-boot:run -pl backend - name: "Frontend" init: | cd frontend corepack enable pnpm install pnpm build command: | gp ports await 9977 cd $GITPOD_REPO_ROOT/frontend pnpm dev ports: - port: 9977 onOpen: open-browser visibility: public - port: 3080 onOpen: open-browser visibility: public vscode: extensions: - redhat.java - vscjava.vscode-java-debug - vscjava.vscode-java-test - pivotal.vscode-spring-boot - graphql.vscode-graphql jetbrains: intellij: plugins: - com.intellij.lang.jsgraphql prebuilds: version: both ================================================ FILE: .mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar ================================================ FILE: .run/Frontend.run.xml ================================================
================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/PetClinicTestDbConfiguration.java ================================================ package org.springframework.samples.petclinic; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.testcontainers.containers.PostgreSQLContainer; @SuppressWarnings("ALL") @TestConfiguration(proxyBeanMethods = false) public class PetClinicTestDbConfiguration { @Bean @ServiceConnection public PostgreSQLContainer postgresContainer() { return new PostgreSQLContainer<>("postgres:16.1-alpine") // https://stackoverflow.com/a/74095511/6134498 .withEnv("POSTGRES_INITDB_ARGS", "--locale-provider=icu --icu-locale=en"); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/AbstractClinicGraphqlTests.java ================================================ package org.springframework.samples.petclinic.graphql; import com.github.dockerjava.api.model.ContainerConfig; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.graphql.test.tester.WebGraphQlTester; import org.springframework.graphql.test.tester.WebSocketGraphQlTester; import org.springframework.http.HttpHeaders; import org.springframework.samples.petclinic.PetClinicTestDbConfiguration; import org.springframework.samples.petclinic.security.JwtTokenService; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; @SpringBootTest @AutoConfigureMockMvc @AutoConfigureHttpGraphQlTester @Import(PetClinicTestDbConfiguration.class) @Transactional public class AbstractClinicGraphqlTests extends GraphQlTokenProvider { protected WebGraphQlTester managerRoleGraphQlTester; protected WebGraphQlTester userRoleGraphQlTester; protected WebGraphQlTester unauthorizedGraphqlTester; @BeforeEach void setupWebGraphqlTester(@Autowired WebGraphQlTester graphQlTester) { this.unauthorizedGraphqlTester = graphQlTester; this.userRoleGraphQlTester = graphQlTester.mutate() .headers(this::withUserToken) .build(); this.managerRoleGraphQlTester = graphQlTester.mutate() .headers(this::withManagerToken) .build(); } private void withManagerToken(HttpHeaders headers) { headers.setBearerAuth(createManagerToken()); } private void withUserToken(HttpHeaders headers) { headers.setBearerAuth(createUserToken()); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/AuthControllerTests.java ================================================ package org.springframework.samples.petclinic.graphql; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class AuthControllerTests extends AbstractClinicGraphqlTests { @Test void shouldReturnCurrentUser() { userRoleGraphQlTester .documentName("meQuery") .execute() .path("me.username").entity(String.class).isEqualTo("joe") .path("me.fullname").entity(String.class).isEqualTo("Joe Hill"); } @Test void shouldReturnUnauthorizedWithoutToken() { assertThatThrownBy(() -> unauthorizedGraphqlTester.mutate() .build() .documentName("meQuery") .executeAndVerify()) .hasMessage("Status expected:<200 OK> but was:<401 UNAUTHORIZED>"); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/GraphQlTokenProvider.java ================================================ package org.springframework.samples.petclinic.graphql; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.samples.petclinic.security.JwtTokenService; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; public class GraphQlTokenProvider { @Autowired private JwtTokenService tokenService; protected String createManagerToken() { return tokenService.generateToken("susi", List.of( () -> "MANAGER"), Instant.now().plus(1, ChronoUnit.HOURS)); } protected String createUserToken() { return tokenService.generateToken("joe", List.of( () -> "USER"), Instant.now().plus(1, ChronoUnit.HOURS)); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/OwnerControllerTests.java ================================================ package org.springframework.samples.petclinic.graphql; import org.junit.jupiter.api.Test; import org.springframework.graphql.execution.ErrorType; import static org.assertj.core.api.Assertions.assertThat; public class OwnerControllerTests extends AbstractClinicGraphqlTests{ @Test public void owners_no_sort_order() { // language=GraphQL var query = """ query { owners(first: 5) { edges { node { id } } } } """; // by default results are orderd by id userRoleGraphQlTester.document(query) .execute() .path("owners.edges").entityList(Object.class).hasSize(5) .path("owners.edges[0].node.id").entity(Integer.class).isEqualTo(1) .path("owners.edges[1].node.id").entity(Integer.class).isEqualTo(2) .path("owners.edges[2].node.id").entity(Integer.class).isEqualTo(3) .path("owners.edges[3].node.id").entity(Integer.class).isEqualTo(4) .path("owners.edges[4].node.id").entity(Integer.class).isEqualTo(5) ; } @Test public void owners_after() { // language=GraphQL var query = """ query { owners(first: 5, after:"T18z") { edges { node { id } } } } """; // by default results are orderd by id userRoleGraphQlTester.document(query) .execute() .path("owners.edges").entityList(Object.class).hasSize(5) .path("owners.edges[0].node.id").entity(Integer.class).isEqualTo(4) .path("owners.edges[1].node.id").entity(Integer.class).isEqualTo(5) .path("owners.edges[2].node.id").entity(Integer.class).isEqualTo(6) .path("owners.edges[3].node.id").entity(Integer.class).isEqualTo(7) .path("owners.edges[4].node.id").entity(Integer.class).isEqualTo(8) ; } @Test public void owners_order_by_lastname() { // language=GraphQL var query = """ query { owners(first: 3, after: "T180", order: [{field: lastName}]) { edges { node { id lastName firstName } } } } """; userRoleGraphQlTester.document(query) .execute() .path("owners.edges").entityList(Object.class).hasSize(3) .path("owners.edges[0].node.id").entity(Integer.class).isEqualTo(6) .path("owners.edges[1].node.id").entity(Integer.class).isEqualTo(17) .path("owners.edges[2].node.id").entity(Integer.class).isEqualTo(2) ; } @Test public void owners_order_by_lastname_and_firstname() { // language=GraphQL var query = """ query { owners(first: 3, after: "T180", order: [{field: lastName}, {field:firstName, direction:DESC}]) { edges { node { id lastName firstName } } } } """; userRoleGraphQlTester.document(query) .execute() .path("owners.edges").entityList(Object.class).hasSize(3) .path("owners.edges[0].node.id").entity(Integer.class).isEqualTo(6) .path("owners.edges[1].node.id").entity(Integer.class).isEqualTo(17) .path("owners.edges[2].node.id").entity(Integer.class).isEqualTo(4) ; } @Test public void owners_order_by_and_filter() { // language=GraphQL var query = """ query { owners(first: 10, order: [{field: lastName}] filter: {lastName: "du"}, after: "T18x") { edges { node { id lastName firstName } } } } """; userRoleGraphQlTester.document(query) .execute() .path("owners.edges").entityList(Object.class).hasSize(2) .path("owners.edges[0].node.id").entity(Integer.class).isEqualTo(30) .path("owners.edges[1].node.id").entity(Integer.class).isEqualTo(21) ; } @Test public void owners_filter() { // language=GraphQL var query = """ query { owners(first: 10, filter: {lastName: "du"}, after: "T18x") { edges { node { id lastName firstName } } } } """; userRoleGraphQlTester.document(query) .execute() .path("owners.edges").entityList(Object.class).hasSize(2) .path("owners.edges[0].node.id").entity(Integer.class).isEqualTo(21) .path("owners.edges[1].node.id").entity(Integer.class).isEqualTo(30) ; } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/PetControllerTests.java ================================================ package org.springframework.samples.petclinic.graphql; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.samples.petclinic.repository.PetRepository; import org.springframework.test.annotation.DirtiesContext; import java.util.Map; public class PetControllerTests extends AbstractClinicGraphqlTests { @Test public void petsQuery_shouldReturnAllPets() { userRoleGraphQlTester.document("query { pets { id name } }") .execute() .path("data.pets[*]").entityList(Object.class).hasSize(70) .path("data.pets[0].id").hasValue() .path("data.pets[0].name").hasValue(); } @Test public void petByIdQuery_shouldReturnPet() { userRoleGraphQlTester.document("query { pet(id: 3) { id name } }") .execute() .path("data.pet.id").entity(String.class).isEqualTo("3") .path("data.pet.name").entity(String.class).isEqualTo("Rosy"); } @Test public void petByIdQuery_shouldReturnNullForUnknownPet() { userRoleGraphQlTester.document("query { pet(id: 666) { id name } }") .execute() .path("data.pet").valueIsNull(); } @Test public void pet_shouldIncludeVisits() { userRoleGraphQlTester .document("query { pet(id: 8) { id visits { totalCount visits { id } } } }") .execute() .path("data.pet.visits.totalCount").entity(int.class).isEqualTo(2) .path("data.pet.visits[*]").entityList(Object.class).hasSize(2) .path("data.pet.visits.visits[0].id").entity(String.class).isEqualTo("2") .path("data.pet.visits.visits[1].id").entity(String.class).isEqualTo("3"); } @Test public void addPetMutation_shouldAddNewPet() { userRoleGraphQlTester.documentName("addPetMutation") .execute() .path("data.addPet.pet.id").hasValue() .path("data.addPet.pet.birthDate").entity(String.class).isEqualTo("2019/03/17") .path("data.addPet.pet.owner.id").entity(String.class).isEqualTo("2") .path("data.addPet.pet.type.id").entity(String.class).isEqualTo("3") .path("data.addPet.pet.visits.totalCount").entity(int.class).isEqualTo(0); userRoleGraphQlTester.document("query { pets { id } }") .execute() .path("data.pets[*]").entityList(Object.class).hasSize(71); } @Test public void updatePetMutation_shouldUpdatePet() { userRoleGraphQlTester.documentName("updatePetMutation") .variable("updatePetInput", Map.of( "petId", 1, // "birthDate", "2022/03/29" // ) ) .execute() .path("data.updatePet.pet.id").entity(String.class).isEqualTo("1") .path("data.updatePet.pet.birthDate").entity(String.class).isEqualTo("2022/03/29") .path("data.updatePet.pet.owner.id").entity(String.class).isEqualTo("1") .path("data.updatePet.pet.type.id").entity(String.class).isEqualTo("1"); userRoleGraphQlTester.documentName("updatePetMutation") .variable("updatePetInput", Map.of( "petId", 4, // "typeId", 3 ) ) .execute() .path("data.updatePet.pet.id").entity(String.class).isEqualTo("4") .path("data.updatePet.pet.type.id").entity(String.class).isEqualTo("3"); userRoleGraphQlTester.documentName("updatePetMutation") .variable("updatePetInput", Map.of( "petId", 4, // "typeId", 3 ) ) .execute() .path("data.updatePet.pet.id").entity(String.class).isEqualTo("4") .path("data.updatePet.pet.type.id").entity(String.class).isEqualTo("3"); userRoleGraphQlTester.documentName("updatePetMutation") .variable("updatePetInput", Map.of( "petId", 5, // "name", "Klaus-Dieter" ) ) .execute() .path("data.updatePet.pet.id").entity(String.class).isEqualTo("5") .path("data.updatePet.pet.name").entity(String.class).isEqualTo("Klaus-Dieter"); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/PetTypeControllerTests.java ================================================ package org.springframework.samples.petclinic.graphql; import org.junit.jupiter.api.Test; public class PetTypeControllerTests extends AbstractClinicGraphqlTests { @Test void pettypesQuery_shouldReturnAllPetTypes() { this.userRoleGraphQlTester.document("query { pettypes { id name } }") .execute() .path("data.pettypes[*]").entityList(Object.class).hasSize(6) .path("data.pettypes[1].id").hasValue() .path("data.pettypes[1].name").hasValue(); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/SpecialtyControllerTests.java ================================================ package org.springframework.samples.petclinic.graphql; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.graphql.test.tester.GraphQlTester; import org.springframework.test.annotation.DirtiesContext; public class SpecialtyControllerTests extends AbstractClinicGraphqlTests { @Test public void specialtiesQueryReturnsList() { String query = "query {" + " specialties {" + " id" + " name" + " }" + "}"; userRoleGraphQlTester .document(query) .execute() .path("specialties").entityList(Object.class).hasSizeGreaterThan(2); ; } @Test public void updateSpecialtyWorks() { String query = "mutation {" + " updateSpecialty(input: {specialtyId: 1, name: \"test\"}) {" + " specialty {" + " id" + " name" + " }" + " }" + "}"; userRoleGraphQlTester .document(query) .execute() .path("updateSpecialty.specialty.name").entity(String.class).isEqualTo("test"); ; } @Test public void addSpecialtyWorks() { String query = "mutation {" + " addSpecialty(input: {name: \"xxx\"}) {" + " specialty {" + " id" + " name" + " }" + " }" + "}"; userRoleGraphQlTester .document(query) .execute() .path("addSpecialty.specialty.name").entity(String.class).isEqualTo("xxx"); ; } @Test public void addAndRemoveSpecialtyWorks() { String getQuery = "query {" + " specialties {" + " id" + " name" + " }" + "}"; final int specialtyCount = userRoleGraphQlTester .document(getQuery) .execute() .path("specialties").entityList(Object.class).get().size(); String query = "mutation {" + " addSpecialty(input: {name: \"yyy\"}) {" + " specialty {" + " id" + " name" + " }" + " }" + "}"; GraphQlTester.Response response = userRoleGraphQlTester .document(query) .execute(); response .path("addSpecialty.specialty.name").entity(String.class).isEqualTo("yyy"); String id = response.path("addSpecialty.specialty.id").entity(String.class).get(); Assertions.assertNotNull(id); String removeQuery = "mutation {" + " removeSpecialty(input: {specialtyId: " + id + "}) {" + " specialties {" + " id" + " }" + " }" + "}"; this.userRoleGraphQlTester.document(removeQuery) .execute() .path("removeSpecialty.specialties").entityList(Object.class).hasSize(specialtyCount); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/VetControllerTests.java ================================================ package org.springframework.samples.petclinic.graphql; import org.junit.jupiter.api.Test; import org.springframework.graphql.execution.ErrorType; import static org.assertj.core.api.Assertions.assertThat; public class VetControllerTests extends AbstractClinicGraphqlTests{ /** * EXAMPLE: * -------------------------- * * Mutation returning a union type (AddVetPayload) with data or error, returns data if invoked correctly */ @Test void shouldAddNewVet() { managerRoleGraphQlTester .documentName("addVetMutation") .variable("specialtyIds", new int[]{1, 3}) .execute() .path("addVet.vet.id").hasValue() .path("addVet.vet.firstName").entity(String.class).isEqualTo("Klaus") .path("addVet.vet.lastName").entity(String.class).isEqualTo("Smith") .path("addVet.vet.specialties[*]").entityList(Object.class).hasSize(2) .path("addVet.vet.specialties[0].id").entity(String.class).isEqualTo("3") .path("addVet.vet.specialties[1].id").entity(String.class).isEqualTo("1"); } /** * EXAMPLE: * -------------------------- * * Mutation returning a union type (AddVetPayload) with data or error, returns "domain" error * type if invoked correctly */ @Test void shouldReturnErrorPayloadOnUnknownSpecialty() { managerRoleGraphQlTester .documentName("addVetMutation") .variable("specialtyIds", new int[]{666}) .execute() .path("addVet.vet").pathDoesNotExist() .path("addVet.error").entity(String.class).isEqualTo("Specialty with Id '666' not found"); } /** * EXAMPLE: * -------------------------- * * Mutation is secured using fine-grained security with @PreAuth */ @Test void shouldForbidAddingVetsAsUser() { userRoleGraphQlTester .documentName("addVetMutation") .variable("specialtyIds", new int[]{1, 3}) .execute() .errors() .satisfy(errors -> { assertThat(errors).hasSize(1); assertThat(errors.get(0).getErrorType()).isEqualTo(ErrorType.FORBIDDEN); }); } @Test public void vetsReturnsListOfAllVets() { // language=GraphQL String query = """ query { vets { edges { node { id lastName firstName visits { visits { id pet { id } } } } cursor } pageInfo { hasNextPage } } } """ ; userRoleGraphQlTester.document(query) .execute() .path("vets.edges").entityList(Object.class).hasSize(10) .path("vets.edges[0].node.id").entity(int.class).isEqualTo(1) .path("vets.edges[1].node.id").entity(int.class).isEqualTo(3) .path("vets.edges[1].node.lastName").entity(String.class).isEqualTo("Douglas") .path("vets.edges[5].node.id").entity(int.class).isEqualTo(4) .path("vets.edges[5].node.visits.visits").entityList(Object.class).hasSize(2) .path("vets.edges[5].node.visits.visits[0].id").entity(int.class).isEqualTo(1) .path("vets.edges[5].node.visits.visits[0].pet.id").entity(int.class).isEqualTo(7) .path("vets.edges[5].node.visits.visits[1].id").entity(int.class).isEqualTo(3) .path("vets.edges[5].node.visits.visits[1].pet.id").entity(int.class).isEqualTo(8) .path("vets.edges[8].node.id").entity(int.class).isEqualTo(5) .path("vets.edges[9].node.id").entity(int.class).isEqualTo(9) .path("vets.pageInfo.hasNextPage").entity(boolean.class).isEqualTo(false) ; } @Test public void vetReturnsVetById() { String query = "query {" + " vet(id:4) {" + " id" + " specialties {" + " id" + " }"+ " visits {" + " totalCount" + " visits {" + " id" + " pet {" + " id" + " }" + " }" + " }" + " }" + "}"; userRoleGraphQlTester.document(query) .execute() .path("vet.specialties[0].id").entity(int.class).isEqualTo(2) .path("vet.visits.totalCount").entity(int.class).isEqualTo(2) .path("vet.visits.visits[0].id").entity(String.class).isEqualTo("1") .path("vet.visits.visits[0].pet.id").entity(String.class).isEqualTo("7") ; } @Test public void vetReturnsNullIfNotFound() { String query = "query {" + " vet(id:666) {" + " id" + " specialties {" + " id" + " }"+ " visits {" + " totalCount" + " visits {" + " id" + " pet {" + " id" + " }" + " }" + " }" + " }" + "}"; userRoleGraphQlTester.document(query) .execute() .path("vet").valueIsNull(); ; } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/VisitControllerTests.java ================================================ package org.springframework.samples.petclinic.graphql; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.graphql.server.WebGraphQlHandler; import org.springframework.graphql.test.tester.GraphQlTester; import org.springframework.graphql.test.tester.WebGraphQlTester; import org.springframework.graphql.test.tester.WebSocketGraphQlTester; import org.springframework.samples.petclinic.PetClinicTestDbConfiguration; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; import org.springframework.web.reactive.socket.client.WebSocketClient; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import java.net.URI; import java.util.Map; public class VisitControllerTests extends AbstractClinicGraphqlTests { private static final Logger log = LoggerFactory.getLogger( VisitControllerTests.class ); @Test public void visit_shouldIncludeTreatingVet() { userRoleGraphQlTester .document("query { pet(id: 8) { id visits { visits { id treatingVet { id } } } } }") .execute() .path("data.pet.visits.visits[*]").entityList(Object.class).hasSize(2) .path("data.pet.visits.visits[0].id").entity(String.class).isEqualTo("2") .path("data.pet.visits.visits[0].treatingVet").valueIsNull() .path("data.pet.visits.visits[1].id").entity(String.class).isEqualTo("3") .path("data.pet.visits.visits[1].treatingVet.id").entity(String.class).isEqualTo("4"); } @Test void shouldAddNewVisit() { userRoleGraphQlTester .documentName("addVisitMutation") .execute() .path("addVisit.visit.description").entity(String.class).isEqualTo("hurray") .path("addVisit.visit.date").entity(String.class).isEqualTo("2020/12/31") .path("addVisit.visit.pet.id").entity(String.class).isEqualTo("1"); } @Test void shouldAddNewVisitFromVariables_And_HandlesDateCoercingInVariables(@Autowired WebGraphQlTester graphQlTester) { userRoleGraphQlTester .documentName("addVisitMutationWithVariables") .variable("addVisitInput", Map.of( "petId", 3,// "description", "Another visit", // "date", "2022/03/25" )) .execute() .path("addVisit.visit.description").entity(String.class).isEqualTo("Another visit") .path("addVisit.visit.date").entity(String.class).isEqualTo("2022/03/25") .path("addVisit.visit.pet.id").entity(String.class).isEqualTo("3") .path("addVisit.visit.treatingVet").valueIsNull(); } @Test void shouldAddNewVisitFromVariablesWithVetId(@Autowired WebGraphQlTester graphQlTester) { userRoleGraphQlTester .documentName("addVisitMutationWithVariables") .variable("addVisitInput", Map.of( "petId", 3,// "description", "Another visit", // "date", "2022/03/25", "vetId", 1 )) .execute() .path("addVisit.visit.description").entity(String.class).isEqualTo("Another visit") .path("addVisit.visit.date").entity(String.class).isEqualTo("2022/03/25") .path("addVisit.visit.pet.id").entity(String.class).isEqualTo("3") .path("addVisit.visit.treatingVet.id").entity(String.class).isEqualTo("1"); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/graphql/VisitSubscriptionTest.java ================================================ package org.springframework.samples.petclinic.graphql; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; import org.springframework.graphql.server.WebGraphQlHandler; import org.springframework.graphql.test.tester.GraphQlTester; import org.springframework.graphql.test.tester.WebGraphQlTester; import org.springframework.graphql.test.tester.WebSocketGraphQlTester; import org.springframework.samples.petclinic.PetClinicTestDbConfiguration; import org.springframework.samples.petclinic.model.VisitService; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; import org.springframework.web.reactive.socket.client.WebSocketClient; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import java.time.LocalDate; import java.util.Optional; import static java.lang.String.format; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Import(PetClinicTestDbConfiguration.class) public class VisitSubscriptionTest extends GraphQlTokenProvider { private static final Logger log = LoggerFactory.getLogger(VisitSubscriptionTest.class); @LocalServerPort int port; @Autowired VisitService visitService; @Test void onNewVisit_for_new_visits(@Autowired GraphQlProperties graphQlProperties) { String url = format("http://localhost:%s/%s?access_token=%s", port, graphQlProperties.getWebsocket().getPath(), createUserToken()); // https://docs.spring.io/spring-graphql/reference/testing.html#testing.subscriptions WebSocketClient client = new ReactorNettyWebSocketClient(); WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build(); Flux visitSubscription = tester. // language=GraphQL document(""" subscription { onNewVisit { id description pet { id } treatingVet { id } } } """) .executeSubscription() .toFlux(); // var newVisit = visitService.addVisit(2, "New Visit for Subscription", LocalDate.now(), Optional.of(1)); StepVerifier. create(visitSubscription) .consumeNextWith(r -> { r.path("onNewVisit.id").entity(Integer.class).isEqualTo(newVisit.getId()) .path("onNewVisit.description").entity(String.class).isEqualTo("New Visit for Subscription") .path("onNewVisit.pet.id").entity(Integer.class).isEqualTo(2) .path("onNewVisit.treatingVet.id").entity(Integer.class).isEqualTo(1); }).thenCancel().verify(); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java ================================================ /* * Copyright 2012-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.samples.petclinic.model; import org.junit.jupiter.api.Test; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validator; import java.util.Locale; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; /** * @author Michael Isvy Simple test to make sure that Bean Validation is working (useful * when upgrading to a new version of Hibernate Validator/ Bean Validation) */ class ValidatorTests { private Validator createValidator() { LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); localValidatorFactoryBean.afterPropertiesSet(); return localValidatorFactoryBean; } @Test void shouldNotValidateWhenFirstNameEmpty() { LocaleContextHolder.setLocale(Locale.ENGLISH); Person person = new Person(); person.setFirstName(""); person.setLastName("smith"); Validator validator = createValidator(); Set> constraintViolations = validator.validate(person); assertThat(constraintViolations).hasSize(1); ConstraintViolation violation = constraintViolations.iterator().next(); assertThat(violation.getPropertyPath().toString()).isEqualTo("firstName"); assertThat(violation.getMessage()).isEqualTo("must not be empty"); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/repository/ApplicationTestConfig.java ================================================ package org.springframework.samples.petclinic.repository; import org.mockito.MockitoAnnotations; import org.springframework.boot.test.context.TestConfiguration; @TestConfiguration public class ApplicationTestConfig { public ApplicationTestConfig(){ MockitoAnnotations.openMocks(this); } } ================================================ FILE: backend/src/test/java/org/springframework/samples/petclinic/repository/ClinicRepositorySpringDataJpaTests.java ================================================ /* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.samples.petclinic.repository; import jakarta.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.data.domain.*; import org.springframework.data.jpa.domain.Specification; import org.springframework.samples.petclinic.PetClinicTestDbConfiguration; import org.springframework.samples.petclinic.model.*; import org.springframework.samples.petclinic.util.EntityUtils; import org.springframework.test.context.ContextConfiguration; import org.springframework.transaction.annotation.Transactional; import java.util.Collection; import java.util.Date; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.springframework.samples.petclinic.model.OwnerFilter.NO_FILTER; /** *

Base class for Repository integration tests.

Subclasses should specify Spring context * configuration using {@link ContextConfiguration @ContextConfiguration} annotation

* AbstractclinicServiceTests and its subclasses benefit from the following services provided by the Spring * TestContext Framework:

  • Spring IoC container caching which spares us unnecessary set up * time between test execution.
  • Dependency Injection of test fixture instances, meaning that * we don't need to perform application context lookups. *
  • Transaction management, meaning each test method is executed in its own transaction, * which is automatically rolled back by default. Thus, even if tests insert or otherwise change database state, there * is no need for a teardown or cleanup script.
  • An {@link org.springframework.context.ApplicationContext * ApplicationContext} is also inherited and can be used for explicit bean lookup if necessary.
* * @author Ken Krebs * @author Rod Johnson * @author Juergen Hoeller * @author Sam Brannen * @author Michael Isvy * @author Vitaliy Fedoriv */ @SpringBootTest @Transactional @Import(PetClinicTestDbConfiguration.class) class ClinicRepositorySpringDataJpaTests { @Autowired EntityManager entityManager; @Autowired private PetRepository petRepository; @Autowired private VetRepository vetRepository; @Autowired private OwnerRepository ownerRepository; @Autowired private VisitRepository visitRepository; @Autowired private SpecialtyRepository specialtyRepository; @Autowired private PetTypeRepository petTypeRepository; @BeforeEach void init() { MockitoAnnotations.openMocks(this); } @Test void shouldFindFall() { OwnerFilter filter = new OwnerFilter(); filter.setLastName("Escobito"); var page = ownerRepository.findAll(filter, PageRequest.ofSize(10)); assertThat(page.getTotalElements()).isEqualTo(1L); assertThat(page.getContent().get(0).getLastName()).isEqualTo("Escobito"); filter = new OwnerFilter(); // lastname search is "Starts with", i.e. all names that start // with "es" should be returned (case insensitive) filter.setLastName("es"); page = ownerRepository.findAll(filter, PageRequest.ofSize(10)); assertThat(page.getTotalElements()).isEqualTo(2L); } @Test void shouldFindSingleOwnerWithPet() { Owner owner = this.ownerRepository.findById(1).orElseThrow(); assertThat(owner.getLastName()).startsWith("Franklin"); assertThat(owner.getPets().size()).isEqualTo(1); assertThat(owner.getPets().get(0).getType()).isNotNull(); assertThat(owner.getPets().get(0).getType().getName()).isEqualTo("Cat"); } @Test void shouldInsertOwner() { Owner owner = new Owner(); owner.setFirstName("Sam"); owner.setLastName("Schultz"); owner.setAddress("4, Evans Street"); owner.setCity("Wollongong"); owner.setTelephone("4444444444"); this.ownerRepository.save(owner); assertThat(owner.getId().longValue()).isNotEqualTo(0); } @Test void shouldUpdateOwner() { Owner owner = this.ownerRepository.findById(1).orElseThrow(); String oldLastName = owner.getLastName(); String newLastName = oldLastName + "X"; owner.setLastName(newLastName); this.ownerRepository.save(owner); // retrieving new name from database owner = this.ownerRepository.findById(1).orElseThrow(); assertThat(owner.getLastName()).isEqualTo(newLastName); } @Test void shouldFindPetWithCorrectId() { Pet pet7 = this.petRepository.findById(7).orElseThrow(); assertThat(pet7.getName()).startsWith("Samantha"); assertThat(pet7.getOwner().getFirstName()).isEqualTo("Jean"); } @Test void shouldReturnEmptyOnMissingPet() { var unknownPet = this.petRepository.findById(7777); assertFalse(unknownPet.isPresent()); } @Test void shouldInsertPetIntoDatabaseAndGenerateId() { Owner owner6 = this.ownerRepository.findById(6).orElseThrow(); int found = owner6.getPets().size(); Pet pet = new Pet(); pet.setName("bowser"); Collection types = this.petRepository.findPetTypes(); pet.setType(EntityUtils.getById(types, PetType.class, 2)); pet.setBirthDate(EntityUtils.asDateTime(new Date())); owner6.addPet(pet); assertThat(owner6.getPets().size()).isEqualTo(found + 1); this.petRepository.save(pet); this.ownerRepository.save(owner6); owner6 = this.ownerRepository.findById(6).orElseThrow(); assertThat(owner6.getPets().size()).isEqualTo(found + 1); // checks that id has been generated assertThat(pet.getId()).isNotNull(); } @Test void shouldUpdatePetName() { Pet pet7 = this.petRepository.findById(7).orElseThrow(); String oldName = pet7.getName(); String newName = oldName + "X"; pet7.setName(newName); this.petRepository.save(pet7); pet7 = this.petRepository.findById(7).orElseThrow(); assertThat(pet7.getName()).isEqualTo(newName); } @Test void shouldFindVets() { var vets = this.vetRepository.findBy(ScrollPosition.offset(), Sort.by("lastName", "firstName"), Limit.of(2)); assertThat(vets.size()).isEqualTo(2); assertThat(vets.getContent().get(0).getId()).isEqualTo(1); assertThat(vets.getContent().get(1).getId()).isEqualTo(3); } @Test void shouldAddNewVisitForPet() { Pet pet7 = this.petRepository.findById(7).orElseThrow(); int found = pet7.getVisits().size(); Visit visit = new Visit(); pet7.addVisit(visit); visit.setDescription("test"); this.visitRepository.save(visit); this.petRepository.save(pet7); pet7 = this.petRepository.findById(7).orElseThrow(); assertThat(pet7.getVisits().size()).isEqualTo(found + 1); assertThat(visit.getId()).isNotNull(); } @Test void shouldFindVisitsByPetId() throws Exception { Collection visits = this.visitRepository.findByPetIdOrderById(7); assertThat(visits.size()).isEqualTo(2); Visit[] visitArr = visits.toArray(new Visit[visits.size()]); assertThat(visitArr[0].getPet()).isNotNull(); assertThat(visitArr[0].getDate()).isNotNull(); assertThat(visitArr[0].getPet().getId()).isEqualTo(7); } @Test void shouldFindAllPets() { Collection pets = this.petRepository.findAll(); Pet pet1 = EntityUtils.getById(pets, Pet.class, 1); assertThat(pet1.getName()).isEqualTo("Leo"); Pet pet3 = EntityUtils.getById(pets, Pet.class, 3); assertThat(pet3.getName()).isEqualTo("Rosy"); } @Test void shouldDeletePet() { Pet pet = this.petRepository.findById(1).orElseThrow(); this.petRepository.delete(pet); try { pet = this.petRepository.findById(1).orElseThrow(); } catch (Exception e) { pet = null; } assertThat(pet).isNull(); } @Test void shouldFindVisitDyId() { Visit visit = this.visitRepository.findById(1).orElseThrow(); assertThat(visit.getId()).isEqualTo(1); assertThat(visit.getPet().getName()).isEqualTo("Samantha"); } @Test void shouldFindAllVisits() { Collection visits = this.visitRepository.findAll(); Visit visit1 = EntityUtils.getById(visits, Visit.class, 1); assertThat(visit1.getPet().getName()).isEqualTo("Samantha"); Visit visit3 = EntityUtils.getById(visits, Visit.class, 3); assertThat(visit3.getPet().getName()).isEqualTo("Max"); } @Test void shouldInsertVisit() { Collection visits = this.visitRepository.findAll(); int found = visits.size(); Pet pet = this.petRepository.findById(1).orElseThrow(); Visit visit = new Visit(); visit.setPet(pet); visit.setDate(EntityUtils.asDateTime(new Date())); visit.setDescription("new visit"); this.visitRepository.save(visit); assertThat(visit.getId().longValue()).isNotEqualTo(0); visits = this.visitRepository.findAll(); assertThat(visits.size()).isEqualTo(found + 1); } @Test void shouldUpdateVisit() { Visit visit = this.visitRepository.findById(1).orElseThrow(); String oldDesc = visit.getDescription(); String newDesc = oldDesc + "X"; visit.setDescription(newDesc); this.visitRepository.save(visit); visit = this.visitRepository.findById(1).orElseThrow(); assertThat(visit.getDescription()).isEqualTo(newDesc); } @Test void shouldDeleteVisit() { Visit visit = this.visitRepository.findById(1).orElseThrow(); this.visitRepository.delete(visit); visit = this.visitRepository.findById(1).orElse(null); assertThat(visit).isNull(); } @Test void shouldFindVetDyId() { Vet vet = this.vetRepository.findById(1).orElseThrow(); assertThat(vet.getFirstName()).isEqualTo("James"); assertThat(vet.getLastName()).isEqualTo("Carter"); } @Test void shouldInsertVet() { Vet vet = new Vet(); vet.setFirstName("John"); vet.setLastName("Dow"); var newVet = this.vetRepository.save(vet); assertThat(newVet.getId()).isNotNull(); var foundVet = this.vetRepository.findById(newVet.getId()); assertThat(foundVet.isPresent()).isTrue(); } @Test void shouldUpdateVet() { Vet vet = this.vetRepository.findById(1).orElseThrow(); String oldLastName = vet.getLastName(); String newLastName = oldLastName + "X"; vet.setLastName(newLastName); this.vetRepository.save(vet); vet = this.vetRepository.findById(1).orElseThrow(); assertThat(vet.getLastName()).isEqualTo(newLastName); } @Test void shouldDeleteVet() { Vet vet = this.vetRepository.findById(1).orElseThrow(); this.vetRepository.delete(vet); vet = this.vetRepository.findById(1).orElse(null); assertThat(vet).isNull(); } @Test void shouldFindAllOwners() { Collection owners = this.ownerRepository.findAll(); Owner owner1 = EntityUtils.getById(owners, Owner.class, 1); assertThat(owner1.getFirstName()).isEqualTo("George"); Owner owner3 = EntityUtils.getById(owners, Owner.class, 3); assertThat(owner3.getFirstName()).isEqualTo("Eduardo"); } @Test void shouldFindOwnersPaginated() { var owners = this.ownerRepository.findBy(NO_FILTER, c -> c. limit(5) .sortBy(Sort.by("lastName")) .scroll(ScrollPosition.offset())); assertThat(owners.size()).isEqualTo(5); assertThat(owners.getContent().get(0).getId()).isEqualTo(48); assertThat(owners.getContent().get(1).getId()).isEqualTo(7); assertThat(owners.getContent().get(2).getId()).isEqualTo(55); assertThat(owners.getContent().get(3).getId()).isEqualTo(19); assertThat(owners.getContent().get(4).getId()).isEqualTo(6); var newPosition = owners.positionAt(2); owners = this.ownerRepository.findBy(NO_FILTER, c -> c. limit(5) .sortBy(Sort.by("lastName")) .scroll(newPosition)); assertThat(owners.size()).isEqualTo(5); assertThat(owners.getContent().get(0).getId()).isEqualTo(19); assertThat(owners.getContent().get(1).getId()).isEqualTo(6); assertThat(owners.getContent().get(2).getId()).isEqualTo(17); assertThat(owners.getContent().get(3).getId()).isEqualTo(2); assertThat(owners.getContent().get(4).getId()).isEqualTo(4); // with filter var filter = new OwnerFilter(); filter.setLastName("da"); owners = this.ownerRepository.findBy(filter, c -> c. limit(5) .sortBy(Sort.by("lastName")) .scroll(ScrollPosition.offset(1))); assertThat(owners.size()).isEqualTo(2); assertThat(owners.getContent().get(0).getId()).isEqualTo(2); assertThat(owners.getContent().get(1).getId()).isEqualTo(4); } @Test void shouldDeleteOwner() { Owner owner = this.ownerRepository.findById(1).orElseThrow(); this.ownerRepository.delete(owner); try { owner = this.ownerRepository.findById(1).orElseThrow(); } catch (Exception e) { owner = null; } assertThat(owner).isNull(); } @Test void shouldFindPetTypeById() { PetType petType = findPetTypeById(1); assertThat(petType.getName()).isEqualTo("Cat"); } private PetType findPetTypeById(int petTypeId) { return petTypeRepository.findById(petTypeId).orElse(null); } @Test public void shouldFindAllPetTypes() { Collection petTypes = this.petRepository.findPetTypes(); PetType petType1 = EntityUtils.getById(petTypes, PetType.class, 1); assertThat(petType1.getName()).isEqualTo("Cat"); PetType petType3 = EntityUtils.getById(petTypes, PetType.class, 3); assertThat(petType3.getName()).isEqualTo("Lizard"); } @Test public void shouldInsertPetType() { Collection petTypes = this.petTypeRepository.findAll(); int found = petTypes.size(); PetType petType = new PetType(); petType.setName("tiger"); this.petTypeRepository.save(petType); assertThat(petType.getId().longValue()).isNotEqualTo(0); petTypes = this.petTypeRepository.findAll(); assertThat(petTypes.size()).isEqualTo(found + 1); } @Test public void shouldUpdatePetType() { PetType petType = this.petTypeRepository.findById(1).orElseThrow(); String oldLastName = petType.getName(); String newLastName = oldLastName + "X"; petType.setName(newLastName); this.petTypeRepository.save(petType); petType = this.petTypeRepository.findById(1).orElseThrow(); assertThat(petType.getName()).isEqualTo(newLastName); } @Test public void shouldFindSpecialtyById() { Specialty specialty = this.specialtyRepository.findById(1).orElseThrow(); assertThat(specialty.getName()).isEqualTo("radiology"); } @Test public void shouldFindAllSpecialtys() { Collection specialties = this.specialtyRepository.findAll(); Specialty specialty1 = EntityUtils.getById(specialties, Specialty.class, 1); assertThat(specialty1.getName()).isEqualTo("radiology"); Specialty specialty3 = EntityUtils.getById(specialties, Specialty.class, 3); assertThat(specialty3.getName()).isEqualTo("dentistry"); } @Test public void shouldInsertSpecialty() { Collection specialties = this.specialtyRepository.findAll(); int found = specialties.size(); Specialty specialty = new Specialty(); specialty.setName("dermatologist"); this.specialtyRepository.save(specialty); assertThat(specialty.getId().longValue()).isNotEqualTo(0); specialties = this.specialtyRepository.findAll(); assertThat(specialties.size()).isEqualTo(found + 1); } @Test public void shouldUpdateSpecialty() { Specialty specialty = this.specialtyRepository.findById(1).orElseThrow(); String oldLastName = specialty.getName(); String newLastName = oldLastName + "X"; specialty.setName(newLastName); this.specialtyRepository.save(specialty); specialty = this.specialtyRepository.findById(1).orElseThrow(); assertThat(specialty.getName()).isEqualTo(newLastName); } @Test public void shouldDeleteSpecialty() { Specialty specialty = this.specialtyRepository.findById(1).orElseThrow(); this.specialtyRepository.delete(specialty); specialty = this.specialtyRepository.findById(1).orElse(null); assertThat(specialty).isNull(); } } ================================================ FILE: backend/src/test/resources/graphql-test/addPetMutation.graphql ================================================ mutation { addPet(input: { name: "Susi", birthDate: "2019/03/17", ownerId: 2, typeId: 3 }) { pet { birthDate name id type { id } owner { id } visits { totalCount } } } } ================================================ FILE: backend/src/test/resources/graphql-test/addVetMutation.graphql ================================================ mutation($specialtyIds: [Int!]!) { addVet(input: { firstName: "Klaus", lastName: "Smith" specialtyIds: $specialtyIds }) { ... on AddVetErrorPayload { error } ... on AddVetSuccessPayload { vet { id firstName lastName specialties { id name } } } } } ================================================ FILE: backend/src/test/resources/graphql-test/addVisitMutation.graphql ================================================ mutation { addVisit(input:{ petId:1, description:"hurray", date:"2020/12/31", }) { visit { date id description pet { id } } } } ================================================ FILE: backend/src/test/resources/graphql-test/addVisitMutationWithVariables.graphql ================================================ mutation($addVisitInput: AddVisitInput!) { addVisit(input:$addVisitInput) { visit { date id description pet { id } treatingVet { id } } } } ================================================ FILE: backend/src/test/resources/graphql-test/meQuery.graphql ================================================ query { me { username fullname } } ================================================ FILE: backend/src/test/resources/graphql-test/updatePetMutation.graphql ================================================ mutation($updatePetInput: UpdatePetInput!) { updatePet(input: $updatePetInput) { pet { birthDate name id type { id } owner { id } visits { totalCount } } } } ================================================ FILE: build-local.sh ================================================ #! /bin/bash cd petclinic-graphiql && rm -rf ./dist && pnpm build && pnpm copy-to-backend && cd .. ./mvnw -DskipTests -pl backend spring-boot:build-image -Dspring-boot.build-image.imageName=spring-petclinic/petclinic-graphql-backend:0.0.1 cd frontend && rm -rf ./dist && pnpm build && docker build . --tag spring-petclinic/petclinic-graphql-frontend:0.0.1 && cd .. echo "Docker images have been built" echo "You can run the docker-compose file with" echo "docker-compose -f docker-compose-petclinic.yml up -d" ================================================ FILE: docker-compose-petclinic.yml ================================================ version: "3" services: petclinic_graphql_db: image: postgres:16.1-alpine command: ["postgres", "-c", "log_statement=all"] container_name: petclinic_graphql_db environment: - POSTGRES_PASSWORD=secretpw - POSTGRES_USER=klaus - POSTGRES_DB=petclinic_graphql_db - POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=en petclinic_graphql_backend: image: spring-petclinic/petclinic-graphql-backend:0.0.1 container_name: petclinic_graphql_backend depends_on: - petclinic_graphql_db environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://petclinic_graphql_db:5432/petclinic_graphql_db - SPRING_DATASOURCE_USERNAME=klaus - SPRING_DATASOURCE_PASSWORD=secretpw ports: - "3091:9977" petclinic_graphql_frontend: image: spring-petclinic/petclinic-graphql-frontend:0.0.1 container_name: petclinic_graphql_frontend depends_on: - petclinic_graphql_backend ports: - "3090:3090" ================================================ FILE: docker-compose.yml ================================================ version: "3" services: petclinic_graphql_db: image: postgres:16.1-alpine command: ["postgres", "-c", "log_statement=all"] container_name: petclinic_graphql_db ports: - 5432 environment: - POSTGRES_PASSWORD=secretpw - POSTGRES_USER=klaus - POSTGRES_DB=petclinic_graphql_db # https://stackoverflow.com/a/74095511/6134498 - POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=en ================================================ FILE: e2e-tests/.github/workflows/playwright.yml ================================================ name: Playwright Tests on: push: branches: [ main, master ] pull_request: branches: [ main, master ] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - name: Install dependencies run: pnpm install - name: Install Playwright Browsers run: pnpm exec playwright install --with-deps - name: Run Playwright tests run: pnpm exec playwright test - uses: actions/upload-artifact@v3 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 ================================================ FILE: e2e-tests/.gitignore ================================================ node_modules/ /test-results/ /playwright-report/ /playwright/.cache/ ================================================ FILE: e2e-tests/.prettierrc ================================================ { "printWidth": 130 } ================================================ FILE: e2e-tests/package.json ================================================ { "name": "e2e-tests", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "playwright test", "test:ui": "playwright test --ui", "test:headed": "playwright test --headed --project chromium", "test:docker-compose": "PW_FRONTEND_URL=http://localhost:3090 PW_GRAPHIQL_URL=http://localhost:3091 playwright test" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@playwright/test": "^1.39.0", "@types/node": "^20.8.9" }, "dependencies": { "uuid": "^9.0.1" } } ================================================ FILE: e2e-tests/playwright.config.ts ================================================ import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ // require('dotenv').config(); /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ testDir: "./tests", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? [["github"], ["junit", { outputFile: "test-results.xml" }], ["html"]] : "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: process.env.PW_FRONTEND_URL || "http://localhost:3080", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", }, /* Configure projects for major browsers */ projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, { name: "firefox", use: { ...devices["Desktop Firefox"] }, }, { name: "webkit", use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', // use: { ...devices['Pixel 5'] }, // }, // { // name: 'Mobile Safari', // use: { ...devices['iPhone 12'] }, // }, /* Test against branded browsers. */ // { // name: 'Microsoft Edge', // use: { ...devices['Desktop Edge'], channel: 'msedge' }, // }, // { // name: 'Google Chrome', // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // }, ], /* Run your local dev server before starting the tests */ // webServer: { // command: 'npm run start', // url: 'http://127.0.0.1:3000', // reuseExistingServer: !process.env.CI, // }, }); ================================================ FILE: e2e-tests/pom.xml ================================================ 4.0.0 org.springframework.samples.petclinic-graphql e2e-tests 2.0.0 end-to-end tests End-to-end-tests for Spring Petclinic GraphQL Example ================================================ FILE: e2e-tests/tests/graphiql.spec.ts ================================================ import os from "os"; import { test, expect } from "@playwright/test"; const graphiqlUrl = process.env.PW_GRAPHIQL_URL || "http://localhost:9977"; const platform = os.platform(); // https://github.com/microsoft/playwright/issues/16459#issuecomment-1242423005 const controlKey = platform === "darwin" ? "Meta" : "Control"; console.log("platform", platform, "using controlkey", controlKey); test(`customized graphiql at '${graphiqlUrl}' works`, async ({ page }) => { await page.goto(graphiqlUrl); await expect(page).toHaveTitle(/GraphiQL :: Spring PetClinic/i); await page.getByRole("textbox", { name: /username/i }).fill("susi"); await page.getByRole("textbox", { name: /password/i }).fill("susi"); await page.getByRole("button", { name: /login/i }).click(); await page.getByRole("button", { name: /Prettify query/i }).click(); await expect(page.getByText(/query me/i)).toHaveCount(1); await expect(page.getByText(/query twoowners/i)).toHaveCount(1); // Clear editor // await page.getByLabel(/query editor/i).click(); await page.getByLabel("Query Editor").getByRole("textbox").fill(` query MyUsername { me { username } } `); await page.getByRole("button", { name: /headers/i }).click(); await page.getByRole("button", { name: /execute query/i }).click(); await expect(page.getByRole("menuitem", { name: "MyUsername" })).toBeVisible(); await page.getByRole("menuitem", { name: "MyUsername" }).click(); await expect(page.getByLabel("Result Window")).toHaveText(/"username": "susi"/i); }); ================================================ FILE: e2e-tests/tests/owner-detail.spec.ts ================================================ import { expect } from "@playwright/test"; import { petclinicTest } from "./petclinic.fixtures"; import { v4 as uuidv4 } from "uuid"; petclinicTest("owner detail works", async ({ page, loginPage, tableModel }) => { await page.goto("/owners/6"); // "Jean Coleman" await loginPage.login("susi", "susi", /Owners - Jean Coleman/); const contactTable = tableModel(page.getByRole("region", { name: /contact data/i }).locator("table")); await contactTable.expectTableRowContent(0, /name/i, /Jean Coleman/i); await contactTable.expectTableRowContent(1, /Address/i, /105 N. Lake St./i); await contactTable.expectTableRowContent(2, /city/i, /Monona/i); await contactTable.expectTableRowContent(3, /Telephone/i, /6085552654/i); const maxVisitsTable = tableModel(page.getByRole("region", { name: /visits of max/i }).locator("table")); await expect(maxVisitsTable.tableLocator).toBeVisible(); await maxVisitsTable.expectTableRowContent(0, "2013/01/02", "", /rabies shot/); await maxVisitsTable.expectTableRowContent(1, "2013/01/03", "Rafael Ortega", /neutered/); }); petclinicTest("add visit", async ({ context, page, loginPage, tableModel }) => { await page.goto("/owners/7"); // "Jeff Black" await loginPage.login("susi", "susi", /Owners - Jeff Black/); const secondPage = await context.newPage(); await secondPage.goto("/owners/7"); // "Jeff Black" const luckyVisitsTable = tableModel(page.getByRole("region", { name: /visits of lucky/i }).locator("table")); await luckyVisitsTable.tableLocator.isVisible(); const oldVisitCount = await luckyVisitsTable.rows.count(); const secondLuckyVisitsTable = tableModel(secondPage.getByRole("region", { name: /visits of lucky/i }).locator("table")); await expect(secondLuckyVisitsTable.rows).toHaveCount(oldVisitCount); await page.getByRole("button", { name: /Add visit for pet Lucky/i }).click(); const form = page.getByRole("region", { name: /Add visit for pet Lucky/i }); await expect(form).toBeVisible(); const description = `description-${uuidv4()}`; await form.getByLabel(/date/i).fill("2024-08-20"); await form.getByLabel(/description/i).fill(description); await form.getByLabel(/vet/i).selectOption({ label: "Linda Douglas", }); await form.getByRole("button", { name: /save/i }).click(); await luckyVisitsTable.expectTableRowContent(oldVisitCount, "2024/08/20", "Linda Douglas", description); // Check subscription: new created visit // should automatically be visible in second browser tab await secondLuckyVisitsTable.expectTableRowContent(oldVisitCount, "2024/08/20", "Linda Douglas", description); }); ================================================ FILE: e2e-tests/tests/owner-search-page.spec.ts ================================================ import { Locator, Page, expect } from "@playwright/test"; import { TableModel, petclinicTest } from "./petclinic.fixtures"; class OwnerSearchPage { readonly loadMoreButton: Locator; readonly orderAscButton: Locator; readonly orderDescButton: Locator; readonly resultTable: TableModel; constructor(readonly page: Page) { this.loadMoreButton = this.page.getByRole("button", { name: /load more/i }); this.orderAscButton = this.page.getByRole("button", { name: /Order owners by lastname, ascending/i, }); this.orderDescButton = this.page.getByRole("button", { name: /Order owners by lastname, descending/i, }); this.resultTable = new TableModel(page, page.getByRole("table", { name: /Owners/i })); } heading() { return this.page.getByRole("heading", { name: /Search owner/i }); } async find(term: string) { await this.page.getByRole("textbox", { name: /last name/i }).fill(term); await this.page.getByRole("button", { name: /find/i }).click(); } } petclinicTest("owner search works", async ({ page, loginPage }) => { await page.goto("/"); await loginPage.login("susi", "susi"); await page.getByRole("link", { name: "Owners" }).click(); const ownerSearchPage = new OwnerSearchPage(page); await expect(ownerSearchPage.heading()).toBeVisible(); await ownerSearchPage.find("du"); await expect(ownerSearchPage.orderAscButton).toBeDisabled(); await expect(ownerSearchPage.orderDescButton).toBeEnabled(); await expect(ownerSearchPage.loadMoreButton).toBeDisabled(); await expect(ownerSearchPage.resultTable.rows).toHaveCount(3); await ownerSearchPage.resultTable.expectTableRowContent( 0, "Dubois", "Sophie", "456 Rue de la Paix", "Paris", "+33 6 12 34 56 78", "Luna, Ollie" ); await ownerSearchPage.resultTable.expectTableRowContent(1, "Dufresne", "Antoine"); await ownerSearchPage.resultTable.expectTableRowContent(2, "Dupont", "François"); await ownerSearchPage.orderDescButton.click(); await expect(ownerSearchPage.orderAscButton).toBeEnabled(); await expect(ownerSearchPage.orderDescButton).toBeDisabled(); await ownerSearchPage.resultTable.expectTableRowContent(0, "Dupont", "François"); await ownerSearchPage.resultTable.expectTableRowContent(1, "Dufresne", "Antoine"); await ownerSearchPage.resultTable.expectTableRowContent(2, "Dubois", "Sophie"); await ownerSearchPage.orderAscButton.click(); await ownerSearchPage.find("d"); await expect(ownerSearchPage.loadMoreButton).toBeEnabled(); await expect(ownerSearchPage.resultTable.rows).toHaveCount(5); await ownerSearchPage.resultTable.expectTableRowContent(0, "da Silva"); await ownerSearchPage.resultTable.expectTableRowContent(1, "Davis"); await ownerSearchPage.resultTable.expectTableRowContent(2, "Davis"); await ownerSearchPage.resultTable.expectTableRowContent(3, "Dubois"); await ownerSearchPage.resultTable.expectTableRowContent(4, "Dufresne"); await ownerSearchPage.loadMoreButton.click(); await expect(ownerSearchPage.loadMoreButton).toBeDisabled(); await expect(ownerSearchPage.resultTable.rows).toHaveCount(6); await ownerSearchPage.resultTable.expectTableRowContent(0, "da Silva"); // should be unchanged await ownerSearchPage.resultTable.expectTableRowContent(5, "Dupont"); // new fetched item }); ================================================ FILE: e2e-tests/tests/petclinic.fixtures.ts ================================================ import { test as base } from "@playwright/test"; import { expect, type Locator, type Page } from "@playwright/test"; class LoginPage { constructor(readonly page: Page) {} async login(username: string, password: string, expectedAfterLoginHeading: string | RegExp = "Welcome to PetClinic!") { await expect(this.page.getByRole("heading", { name: "Login to PetClinic" })).toBeVisible(); await this.page.getByRole("textbox", { name: /username/i }).fill(username); await this.page.getByRole("textbox", { name: /password/i }).fill(password); await this.page.getByRole("button", { name: /login/i }).click(); await expect(this.page.getByRole("heading", { name: expectedAfterLoginHeading })).toHaveCount(1); } } export class TableModel { readonly rows: Locator; constructor(readonly page: Page, readonly tableLocator: Locator) { this.rows = tableLocator.locator("tbody tr"); } async expectTableRowContent(rowIx: number, ...cellContents: Array) { const row = this.tableLocator.locator("tbody tr").nth(rowIx); await expect(row).toBeVisible(); for (const [ix, content] of cellContents.entries()) { await expect(row.locator("td").nth(ix)).toHaveText(content); } } } type TableFactoryFunction = (tableLocator: Locator) => TableModel; export const petclinicTest = base.extend<{ loginPage: LoginPage; tableModel: TableFactoryFunction; }>({ loginPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await use(loginPage); }, tableModel: async ({ page }, use) => { const table: TableFactoryFunction = (tableLocator) => new TableModel(page, tableLocator); await use(table); }, }); ================================================ FILE: e2e-tests/tests/vets.spec.ts ================================================ import { expect } from "@playwright/test"; import { petclinicTest } from "./petclinic.fixtures"; petclinicTest("vet list works", async ({ page, loginPage, tableModel }) => { await page.goto("/"); await loginPage.login("susi", "susi"); await page.getByRole("link", { name: /VETERINARIANS/i }).click(); const vetTable = tableModel(page.getByRole("table", { name: /All Veterinarians/i })); await expect(vetTable.tableLocator).toBeVisible(); // just a random sample, to make sure table displays anything // and graphql request was successful await vetTable.expectTableRowContent(0, "Carter, James"); await vetTable.expectTableRowContent(1, "Douglas, Linda", "dentistry, surgery"); await vetTable.expectTableRowContent(9, "Tanaka, Akira"); }); petclinicTest("adding vet works", async ({ page, loginPage, tableModel }) => { await page.goto("/vets"); await loginPage.login("susi", "susi", "Manage Veterinarians"); const vetTable = tableModel(page.getByRole("table", { name: /All Veterinarians/i })); await vetTable.expectTableRowContent(0, "Carter, James"); const oldVetCount = await vetTable.rows.count(); // generate a new last name that is for sure at the end of the list of vets // (for the case this tests runs more than one time with the same database) const newLastName = `XXXX-${Date.now()}`; await page.getByRole("button", { name: /add veterinary/i }).click(); await page.getByLabel(/first name/i).fill("Miller"); await page.getByLabel(/last name/i).fill(newLastName); await page.getByLabel(/Specialties/i).selectOption([ { label: "surgery", }, { label: "dentistry", }, ]); await page.getByRole("button", { name: /save/i }).click(); await expect(vetTable.tableLocator).toBeVisible(); await vetTable.expectTableRowContent(oldVetCount, `${newLastName}, Miller`, "dentistry, surgery"); }); petclinicTest("adding vet as user is forbidden", async ({ page, loginPage, tableModel }) => { await page.goto("/vets"); await loginPage.login("joe", "joe", "Manage Veterinarians"); const vetTable = tableModel(page.getByRole("table", { name: /All Veterinarians/i })); await page.getByRole("button", { name: /add veterinary/i }).click(); await page.getByLabel(/first name/i).fill("No"); await page.getByLabel(/last name/i).fill("Way"); await page.getByLabel(/Specialties/i).selectOption([ { label: "surgery", }, ]); await page.getByRole("button", { name: /save/i }).click(); await expect(page.getByText(/forbidden/i)).toBeVisible(); await expect(vetTable.tableLocator).toHaveCount(0); }); ================================================ FILE: frontend/.eslintrc.cjs ================================================ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended", ], ignorePatterns: ["dist", ".eslintrc.cjs", "patch-index-html.js"], parser: "@typescript-eslint/parser", plugins: ["react-refresh"], rules: { "react-refresh/only-export-components": [ "off", { allowConstantExport: true }, ], "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-explicit-any": "off", }, }; ================================================ FILE: frontend/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: frontend/.prettierignore ================================================ .eslintrc.jcs pnpm-lock.yaml ./src/assets/* src/fonts/** ================================================ FILE: frontend/Dockerfile ================================================ FROM nginx:stable-alpine WORKDIR frontend COPY dist /frontend COPY docker/nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 3090 CMD ["nginx", "-g", "daemon off;"] ================================================ FILE: frontend/README.md ================================================ # React + TypeScript + Vite This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Currently, two official plugins are available: - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh ## Expanding the ESLint configuration If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - Configure the top-level `parserOptions` property like this: ```js parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: ['./tsconfig.json', './tsconfig.node.json'], tsconfigRootDir: __dirname, }, ``` - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list ================================================ FILE: frontend/codegen.ts ================================================ import type { CodegenConfig } from "@graphql-codegen/cli"; const config: CodegenConfig = { overwrite: true, schema: "../backend/src/main/resources/graphql/petclinic.graphqls", documents: ["src/**/*.tsx", "src/**/*.graphql"], ignoreNoDocuments: true, generates: { "src/generated/graphql-types.ts": { plugins: [ "typescript", "typescript-operations", "typescript-react-apollo", ], config: { skipTypename: true, preResolveTypes: true, declarationKind: "interface", onlyOperationTypes: true, }, }, }, }; export default config; ================================================ FILE: frontend/docker/nginx.conf ================================================ server { listen 3090; server_name localhost; root /frontend; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; location / { try_files $uri $uri/ /index.html; } location /graphql { proxy_pass http://petclinic_graphql_backend:9977; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; access_log /dev/stdout; error_log /dev/stderr; } location /graphqlws { proxy_pass http://petclinic_graphql_backend:9977; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; access_log /dev/stdout; error_log /dev/stderr; # Optional: Increase the timeout values for WebSocket connections proxy_read_timeout 86400; proxy_send_timeout 86400; } location /api { proxy_pass http://petclinic_graphql_backend:9977; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; access_log /dev/stdout; error_log /dev/stderr; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, max-age=31536000"; } } ================================================ FILE: frontend/index.html ================================================ Spring PetClinic :: GraphQL Edition
================================================ FILE: frontend/package.json ================================================ { "name": "frontend-vite", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "pnpm codegen && tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "check": "pnpm lint && pnpm prettier:check", "prettier:check": "prettier --check .", "prettier:write": "prettier --write .", "codegen": "graphql-codegen --config codegen.ts && prettier --write src/generated/" }, "dependencies": { "@apollo/client": "^3.8.6", "clsx": "^2.0.0", "dayjs": "^1.11.10", "graphql": "^16.8.1", "graphql-ws": "^5.14.2", "immer": "^10.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.47.0", "react-router-dom": "^6.17.0", "tiny-invariant": "^1.3.1" }, "devDependencies": { "@graphql-codegen/cli": "5.0.0", "@graphql-codegen/client-preset": "4.1.0", "@graphql-codegen/typescript-operations": "^4.0.1", "@graphql-codegen/typescript-react-apollo": "^4.0.0", "@tailwindcss/forms": "^0.5.6", "@types/node": "^20.8.7", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", "autoprefixer": "^10.4.16", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "postcss": "^8.4.31", "prettier": "^3.0.3", "prettier-plugin-tailwindcss": "^0.5.6", "tailwindcss": "^3.3.3", "typescript": "^5.0.2", "vite": "^4.4.5" } } ================================================ FILE: frontend/pom.xml ================================================ 4.0.0 org.springframework.samples.petclinic-graphql frontend 2.0.0 spring-petclinic-graphql-frontend Frontend for Spring Petclinic GraphQL Example ================================================ FILE: frontend/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: frontend/prettier.config.cjs ================================================ /** @type {import("prettier").Options} */ const config = { plugins: ["prettier-plugin-tailwindcss"], tailwindFunctions: ["clsx"], }; module.exports = config; ================================================ FILE: frontend/src/App.css ================================================ #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } ================================================ FILE: frontend/src/App.tsx ================================================ import { useAuthToken } from "@/login/AuthTokenProvider.tsx"; import { useMeLazyQuery } from "@/generated/graphql-types.ts"; import LoginPage from "@/login/LoginPage.tsx"; import { AnonymousPageLayout } from "@/components/PageLayout.tsx"; import { Route, Routes } from "react-router-dom"; import WelcomePage from "@/WelcomePage.tsx"; import OwnersPage from "@/owners/OwnerSearchPage.tsx"; import OwnerPage from "@/owners/OwnerPage.tsx"; import VetsPage from "@/vets/VetsPage.tsx"; import NotFoundPage from "@/NotFoundPage.tsx"; import { useEffect } from "react"; function App() { const [token] = useAuthToken(); const [queryMe, { called, loading, error }] = useMeLazyQuery(); useEffect(() => { // don't try to read user data if we don't have token. if (token) { queryMe(); } }, [queryMe, token]); if (!token || error) { return ; } if (loading || !called) { return ; } return ( } /> } /> } /> } /> } /> ); } export default App; ================================================ FILE: frontend/src/NotFoundPage.tsx ================================================ import Heading from "./components/Heading"; import PageLayout from "./components/PageLayout"; export default function NotFoundPage() { return ( Sorry, the requested page could not be found ); } ================================================ FILE: frontend/src/WelcomePage.tsx ================================================ import Heading from "./components/Heading"; import PageLayout from "./components/PageLayout"; import petsImage from "@/assets/pets.png"; export default function WelcomePage() { return ( Welcome Pets ); } ================================================ FILE: frontend/src/assets/readme.md ================================================ Source of Spring Logo SVG: https://upload.wikimedia.org/wikipedia/commons/4/44/Spring_Framework_Logo_2018.svg ================================================ FILE: frontend/src/components/Button.tsx ================================================ import * as React from "react"; import clsx from "clsx"; type ButtonProps = { children: React.ReactNode; disabled?: boolean; onClick?(): void; type?: "primary" | "secondary" | "link"; }; export default function Button({ children, disabled, onClick, type = "primary", ...props }: ButtonProps) { const className = clsx( type === "primary" && `rounded border border-spr-green bg-spr-green px-3.5 py-1.5 font-medium uppercase text-spr-white hover:bg-spr-green-dark disabled:cursor-default disabled:border-spr-green-light disabled:bg-spr-green-light disabled:text-gray-400 `, type === "secondary" && "rounded border border-gray-500 bg-gray-50 px-3.5 py-1.5 font-medium uppercase text-spr-black hover:border-spr-green hover:bg-spr-green-dark hover:text-spr-white", type === "link" && "px-3.5 py-1.5 font-bold uppercase text-spr-blue hover:bg-spr-white hover:underline", ); //const class="hover:text-spr-white disabled:cursor-default disabled:" // const className = // type === "primary" // ? "bg-spr-white text-spr-black uppercase py-1.5 px-3.5 hover:bg-spr-green hover:text-spr-white font-medium font-semibold hover:border-spr-green border-spr-black border rounded disabled:cursor-default" // : "bg-spr-black text-spr-white uppercase py-1 px-3.5 hover:bg-spr-green font-helvetica hover:border-spr-green border-spr-black border disabled:cursor-default "; return ( ); } ================================================ FILE: frontend/src/components/ButtonBar.tsx ================================================ import * as React from "react"; type ButtonBarProps = { align?: "left" | "right" | "center"; children: React.ReactNode; }; export default function ButtonBar({ align = "right", children, }: ButtonBarProps) { const className = `py-3.5 space-x-4 flex flex-row ${ align === "right" ? "justify-end" : align === "center" ? "justify-center" : "" }`; return
{children}
; } ================================================ FILE: frontend/src/components/Card.tsx ================================================ import * as React from "react"; type CardProps = { children: React.ReactNode; fullWidth?: boolean; }; export default function Card({ children, fullWidth }: CardProps) { return (
{children}
); } ================================================ FILE: frontend/src/components/Heading.tsx ================================================ import * as React from "react"; type HeadingProps = { children: React.ReactNode; level?: "2" | "3" | "4"; id?: string; }; export default function Heading({ children, id, level = "2" }: HeadingProps) { switch (level) { case "2": return (

{children}

); case "3": return (

{children}

); case "4": return (

{children}

); } return <>{children}; } ================================================ FILE: frontend/src/components/Input.tsx ================================================ import * as React from "react"; type InputProps = { action?: React.ReactNode; disabled?: boolean; error?: string | boolean | null; id?: string; label: string; name?: string; type?: "text" | "password" | "date"; }; const Input = React.forwardRef(function Input( { action, disabled, error, id, label, name, type = "text", ...rest }, ref, ) { return (
{typeof error === "string" &&

{error}

}
); }); export default Input; ================================================ FILE: frontend/src/components/Label.tsx ================================================ import * as React from "react"; type LabelProps = { children: React.ReactNode; type?: "error" | "info"; }; export default function Label({ children, type = "error" }: LabelProps) { return type === "error" ? (

{children}

) : (

{children}

); } ================================================ FILE: frontend/src/components/Link.tsx ================================================ import * as React from "react"; import { Link as RouterLink } from "react-router-dom"; type LinkProps = { to: string; children: React.ReactNode; }; export default function Link({ to, children }: LinkProps) { return ( {children} ); } ================================================ FILE: frontend/src/components/Nav.tsx ================================================ import * as React from "react"; import { NavLink as RouterNavLink } from "react-router-dom"; import SpringLogo from "@/assets/Spring_Framework_Logo_2018.svg"; import Link from "./Link"; import { useAuthToken } from "@/login/AuthTokenProvider"; import { useLogout } from "@/use-logout"; import { useCurrentUser } from "@/use-current-user-fullname"; import clsx from "clsx"; import { useEffect, useState } from "react"; function NavLogo() { return (
Spring Framework logo
); } type NavBarProps = { nav?: React.ReactElement; mobileMenu?: React.ReactElement; }; export function NavBar({ nav, mobileMenu }: NavBarProps) { return ( ); } type NavLinkProps = { to: string; children: React.ReactNode; exact?: boolean; }; function NavLink({ children, to }: NavLinkProps) { return ( clsx( "h-12 px-4 py-2 text-sm font-bold uppercase text-spr-black hover:border-spr-green hover:bg-spr-white", isActive || "border-t-4 border-spr-green-light", isActive && "border-t-4 border-spr-green bg-spr-white", ) } > {children} ); } export function DefaultNavBar() { const handleSignOut = useLogout(); const { username, fullname } = useCurrentUser(); const [profileMenuOpen, setProfileMenuOpen] = React.useState(false); return (
Home Owners Veterinarians
{!!username && (
{profileMenuOpen && (
Signed in as {fullname}
)}
)}
} mobileMenu={ } /> ); } type ProfileImageProps = { url: string; alt: string; }; function ProfileImage({ url, alt }: ProfileImageProps) { const [token] = useAuthToken(); const [imageData, setImageData] = useState(null); useEffect(() => { if (!token) { setImageData(null); return; } const reader = new FileReader(); fetch(url, { headers: { Authorization: `Bearer ${token}`, }, }) .then((res) => res.blob()) .then( (blob) => new Promise((resolve, reject) => { reader.onload = resolve; reader.onerror = reject; reader.readAsDataURL(blob); }), ) .then((_) => reader.result) .then((imageData) => typeof imageData === "string" ? setImageData(imageData) : setImageData(null), ); }, [token, url]); if (imageData) { return {alt}; } return null; } ================================================ FILE: frontend/src/components/PageHeader.tsx ================================================ import * as React from "react"; type PageHeaderProps = { children: React.ReactNode; }; export default function PageHeader({ children }: PageHeaderProps) { return (

{children}

); } ================================================ FILE: frontend/src/components/PageLayout.tsx ================================================ import * as React from "react"; import PageHeader from "./PageHeader"; import { DefaultNavBar, NavBar } from "./Nav"; type PageLayoutProps = { children?: React.ReactNode; title: string; narrow?: boolean; }; export default function PageLayout({ children, title, narrow, }: PageLayoutProps) { const maxWith = narrow ? "max-w-2xl" : "max-w-7xl"; const className = `${maxWith} mx-auto py-6 sm:px-6 lg:px-8`; return (
{title}
{children}
); } export function AnonymousPageLayout({ children, title, narrow, }: PageLayoutProps) { const maxWith = narrow ? "max-w-2xl" : "max-w-7xl"; const className = `${maxWith} mx-auto py-6 sm:px-6 lg:px-8`; return (
{title}
{children}
); } ================================================ FILE: frontend/src/components/Section.tsx ================================================ import * as React from "react"; import clsx from "clsx"; type SectionProps = { invert?: boolean; narrow?: boolean; children: React.ReactNode; }; export function Section({ children, invert, narrow, ...props }: SectionProps) { const className = clsx( invert ? "mb-4 bg-gray-100 p-4" : "px-4 pb-8 sm:px-0", narrow && "mx-auto max-w-2xl", ); return (
{children}
); } type SectionHeadingProps = { children: React.ReactNode; }; export function SectionHeading({ children }: SectionHeadingProps) { return (
{children}
); } ================================================ FILE: frontend/src/components/Select.tsx ================================================ import * as React from "react"; type SelectOption = { value: string | number; label: string; }; type SelectProps = { defaultValue?: string | number; disabled?: boolean; error?: string | boolean | null; id?: string; label: string; multiple?: boolean; options: SelectOption[]; }; const Select = React.forwardRef(function Input( { defaultValue, disabled, error, id, label, multiple, options, ...rest }, ref, ) { return (
{typeof error === "string" &&

{error}

}
); }); export default Select; ================================================ FILE: frontend/src/components/Table.tsx ================================================ import * as React from "react"; import Heading from "./Heading"; import { useId } from "react"; type TableProps = { title?: string; actions?: React.ReactNode; labels?: string[]; values: React.ReactNode[][]; }; export default function Table({ title, actions, labels, values }: TableProps) { const headingId = useId(); return ( <> {(title || actions) && (
{title && {title}} {actions}
)} {labels && labels.length && ( {labels.map((label, ix) => ( ))} )} {values.map((row, ix) => ( {row.map((col, ix) => ( ))} ))}
{label}
{col}
); } ================================================ FILE: frontend/src/create-graphql-client.ts ================================================ import { ApolloClient, createHttpLink, InMemoryCache, split, } from "@apollo/client"; import { graphqlApiUrl, graphqlWsApiUrl } from "@/urls.ts"; import { setContext } from "@apollo/client/link/context"; import { createClient } from "graphql-ws"; import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; import { getMainDefinition, relayStylePagination, } from "@apollo/client/utilities"; // noinspection JSUnusedLocalSymbols // const token = // "eyJraWQiOiJmYTg5ZmU1OC02ZDk2LTQxNzYtODVmYy1jZmE2ODAzY2IzOWQiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzZWxmIiwic3ViIjoic3VzaSIsImV4cCI6MjAxNTc0NjkwNywiaWF0IjoxNzAwMzg2OTA3LCJzY29wZSI6Ik1BTkFHRVIifQ.ZD_08AtPvJvDfraULVZgH07cgG0OqYRHC8Ie6aWLb802rnpgKjv83l6UE7uSBpMgmqHr2BFGuSwYI5L9wjXuImygF_f4U2sote7zv0M5I2Ou_9CgXaLbAN-NQP19aCaHcRLEQfBH11vGoBsnBtZ_qrmUBdaj-RCrqpuzmmlJAlDYmwhE5hFWt9qSU-SadudBV9rlIikWFEAbUAjJk-m2vdRu_dSVnCQb8DQLnniuSSR11szkmi-BD0BoL0nJQ5PqQ8G2JQUxwQDq2-VPmMPNOX_pbFEPuvKhlgUClqPNW1QWM1A3Vw74KPgGw-9-uUuIia6enxzpOVz69vhKHIxrOr7qebzQ_XwEt46RkKXeA1Vzb-Xemy3Q5ESESN-J1tnwPvJJKwL8UgHUvghLk-ya8uiY7bfORAJZ8nN7riE6eO1UYelpuK6Euk3eTP94De9MjnnV7yvKmay65PIDNO7jqA8UOwJ74LuQ483DJdHog3qh6NafW1IxaOEzrEbSvgGCv_VoypHs5vpITD3sNky1HikNqzVtiptoQaX2Hi5OI-laAq8itqvpF5l5I9lbivy2rReXhj3_m_U_HVDxW8v8zD5PCYxGFEA2GOK9Qu_gVTcMgLJJE-D0e-IkvIk85nC5JimuCsmddGxcxmN_ILeRdZLyM60t7fz8-y-Yx53i5YM"; export function createGraphqlClient() { const httpLink = createHttpLink({ uri: graphqlApiUrl, }); const wsLink = new GraphQLWsLink( createClient({ url: () => { const token = localStorage.getItem("petclinic.token"); console.log("Create WS Link", token); return `${graphqlWsApiUrl}?access_token=${token}`; }, on: { closed: () => console.log("WS closed"), }, }), ); const authLink = setContext((op, { headers }) => { const token = localStorage.getItem("petclinic.token"); console.log("AUTH LINK", op.operationName); return { headers: { ...headers, authorization: token ? `Bearer ${token}` : "", }, }; }); const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === "OperationDefinition" && definition.operation === "subscription" ); }, wsLink, httpLink, ); const client = new ApolloClient({ link: authLink.concat(splitLink), cache: new InMemoryCache({ typePolicies: { Query: { fields: { //https://www.apollographql.com/docs/react/pagination/cursor-based/#relay-style-cursor-pagination owners: relayStylePagination(), }, }, User: { keyFields: ["username"], }, }, }), }); return client; } ================================================ FILE: frontend/src/fonts/README.txt ================================================ TAKEN FROM https://github.com/spring-io/sagan/tree/master/sagan-client/src/fonts ================================================ FILE: frontend/src/fonts/generator_config.txt ================================================ # Font Squirrel Font-face Generator Configuration File # Upload this file to the generator to recreate the settings # you used to create these fonts. {"mode":"optimal","formats":["woff","woff2"],"tt_instructor":"default","fix_gasp":"xy","fix_vertical_metrics":"Y","metrics_ascent":"","metrics_descent":"","metrics_linegap":"","add_spaces":"Y","add_hyphens":"Y","fallback":"none","fallback_custom":"100","options_subset":"basic","subset_custom":"","subset_custom_range":"","subset_ot_features_list":"","css_stylesheet":"stylesheet.css","filename_suffix":"-webfont","emsquare":"2048","spacing_adjustment":"0"} ================================================ FILE: frontend/src/fonts/metropolis-bold-demo.html ================================================ Metropolis Bold Specimen
AaBb
A​B​C​D​E​F​G​H​I​J​K​L​M​N​O​P​Q​R​S​T​U​V​W​X​Y​Z​a​b​c​d​e​f​g​h​i​j​k​l​m​n​o​p​q​r​s​t​u​v​w​x​y​z​1​2​3​4​5​6​7​8​9​0​&​.​,​?​!​@​(​)​#​$​%​*​+​-​=​:​;
10abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
11abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
12abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
13abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
14abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
16abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
18abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
20abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
24abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
30abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
36abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
48abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
60abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
72abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
90abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼body
body
body
body
bodyMetropolis Bold
bodyArial
bodyVerdana
bodyGeorgia

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

Lorem Ipsum Dolor

Etiam porta sem malesuada magna mollis euismod

Donec sed odio dui. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

Pellentesque ornare sem

Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Donec ullamcorper nulla non metus auctor fringilla. Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam id dolor id nibh ultricies vehicula ut id elit.

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Aenean lacinia bibendum nulla sed consectetur.

Nullam quis risus eget urna mollis ornare vel eu leo. Nullam quis risus eget urna mollis ornare vel eu leo. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus auctor fringilla.

Cras mattis consectetur

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean lacinia bibendum nulla sed consectetur. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras mattis consectetur purus sit amet fermentum.

Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu leo. Cras mattis consectetur purus sit amet fermentum.

Language Support

The subset of Metropolis Bold in this kit supports the following languages:
Albanian, Basque, Breton, Chamorro, Danish, Dutch, English, Faroese, Finnish, French, Frisian, Galician, German, Icelandic, Italian, Malagasy, Norwegian, Portuguese, Spanish, Alsatian, Aragonese, Arapaho, Arrernte, Asturian, Aymara, Bislama, Cebuano, Corsican, Fijian, French_creole, Genoese, Gilbertese, Greenlandic, Haitian_creole, Hiligaynon, Hmong, Hopi, Ibanag, Iloko_ilokano, Indonesian, Interglossa_glosa, Interlingua, Irish_gaelic, Jerriais, Lojban, Lombard, Luxembourgeois, Manx, Mohawk, Norfolk_pitcairnese, Occitan, Oromo, Pangasinan, Papiamento, Piedmontese, Potawatomi, Rhaeto-romance, Romansh, Rotokas, Sami_lule, Samoan, Sardinian, Scots_gaelic, Seychelles_creole, Shona, Sicilian, Somali, Southern_ndebele, Swahili, Swati_swazi, Tagalog_filipino_pilipino, Tetum, Tok_pisin, Uyghur_latinized, Volapuk, Walloon, Warlpiri, Xhosa, Yapese, Zulu, Latinbasic, Ubasic, Demo

Glyph Chart

The subset of Metropolis Bold in this kit includes all the glyphs listed below. Unicode entities are included above each glyph to help you insert individual characters into your layout.

&#32;

&#33;

!

&#34;

"

&#35;

#

&#36;

$

&#37;

%

&#38;

&

&#39;

'

&#40;

(

&#41;

)

&#42;

*

&#43;

+

&#44;

,

&#45;

-

&#46;

.

&#47;

/

&#48;

0

&#49;

1

&#50;

2

&#51;

3

&#52;

4

&#53;

5

&#54;

6

&#55;

7

&#56;

8

&#57;

9

&#58;

:

&#59;

;

&#60;

<

&#61;

=

&#62;

>

&#63;

?

&#64;

@

&#65;

A

&#66;

B

&#67;

C

&#68;

D

&#69;

E

&#70;

F

&#71;

G

&#72;

H

&#73;

I

&#74;

J

&#75;

K

&#76;

L

&#77;

M

&#78;

N

&#79;

O

&#80;

P

&#81;

Q

&#82;

R

&#83;

S

&#84;

T

&#85;

U

&#86;

V

&#87;

W

&#88;

X

&#89;

Y

&#90;

Z

&#91;

[

&#92;

\

&#93;

]

&#94;

^

&#95;

_

&#96;

`

&#97;

a

&#98;

b

&#99;

c

&#100;

d

&#101;

e

&#102;

f

&#103;

g

&#104;

h

&#105;

i

&#106;

j

&#107;

k

&#108;

l

&#109;

m

&#110;

n

&#111;

o

&#112;

p

&#113;

q

&#114;

r

&#115;

s

&#116;

t

&#117;

u

&#118;

v

&#119;

w

&#120;

x

&#121;

y

&#122;

z

&#123;

{

&#124;

|

&#125;

}

&#126;

~

&#160;

 

&#161;

¡

&#162;

¢

&#163;

£

&#165;

¥

&#168;

¨

&#173;

­

&#175;

¯

&#180;

´

&#184;

¸

&#191;

¿

&#192;

À

&#193;

Á

&#194;

Â

&#195;

Ã

&#196;

Ä

&#197;

Å

&#198;

Æ

&#199;

Ç

&#200;

È

&#201;

É

&#202;

Ê

&#203;

Ë

&#204;

Ì

&#205;

Í

&#206;

Î

&#207;

Ï

&#208;

Ð

&#209;

Ñ

&#210;

Ò

&#211;

Ó

&#212;

Ô

&#213;

Õ

&#214;

Ö

&#215;

×

&#216;

Ø

&#217;

Ù

&#218;

Ú

&#219;

Û

&#220;

Ü

&#221;

Ý

&#222;

Þ

&#223;

ß

&#224;

à

&#225;

á

&#226;

â

&#227;

ã

&#228;

ä

&#229;

å

&#230;

æ

&#231;

ç

&#232;

è

&#233;

é

&#234;

ê

&#235;

ë

&#236;

ì

&#237;

í

&#238;

î

&#239;

ï

&#240;

ð

&#241;

ñ

&#242;

ò

&#243;

ó

&#244;

ô

&#245;

õ

&#246;

ö

&#247;

÷

&#248;

ø

&#249;

ù

&#250;

ú

&#251;

û

&#252;

ü

&#253;

ý

&#254;

þ

&#255;

ÿ

&#338;

Œ

&#339;

œ

&#376;

Ÿ

&#710;

ˆ

&#732;

˜

&#8192;

 

&#8193;

&#8194;

&#8195;

&#8196;

&#8197;

&#8198;

&#8199;

&#8200;

&#8201;

&#8202;

&#8208;

&#8209;

&#8210;

&#8211;

&#8212;

&#8216;

&#8217;

&#8220;

&#8221;

&#8230;

&#8239;

&#8287;

&#8364;

&#9724;

Installing Webfonts

Webfonts are supported by all major browser platforms but not all in the same way. There are currently four different font formats that must be included in order to target all browsers. This includes TTF, WOFF, EOT and SVG.

1. Upload your webfonts

You must upload your webfont kit to your website. They should be in or near the same directory as your CSS files.

2. Include the webfont stylesheet

A special CSS @font-face declaration helps the various browsers select the appropriate font it needs without causing you a bunch of headaches. Learn more about this syntax by reading the Fontspring blog post about it. The code for it is as follows:

@font-face{ font-family: 'MyWebFont'; src: url('WebFont.eot'); src: url('WebFont.eot?#iefix') format('embedded-opentype'), url('WebFont.woff') format('woff'), url('WebFont.ttf') format('truetype'), url('WebFont.svg#webfont') format('svg'); }

We've already gone ahead and generated the code for you. All you have to do is link to the stylesheet in your HTML, like this:

<link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8" />

3. Modify your own stylesheet

To take advantage of your new fonts, you must tell your stylesheet to use them. Look at the original @font-face declaration above and find the property called "font-family." The name linked there will be what you use to reference the font. Prepend that webfont name to the font stack in the "font-family" property, inside the selector you want to change. For example:

p { font-family: 'WebFont', Arial, sans-serif; }

4. Test

Getting webfonts to work cross-browser can be tricky. Use the information in the sidebar to help you if you find that fonts aren't loading in a particular browser.

================================================ FILE: frontend/src/fonts/metropolis-extrabold-demo.html ================================================ Metropolis Extra Bold Regular Specimen
AaBb
A​B​C​D​E​F​G​H​I​J​K​L​M​N​O​P​Q​R​S​T​U​V​W​X​Y​Z​a​b​c​d​e​f​g​h​i​j​k​l​m​n​o​p​q​r​s​t​u​v​w​x​y​z​1​2​3​4​5​6​7​8​9​0​&​.​,​?​!​@​(​)​#​$​%​*​+​-​=​:​;
10abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
11abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
12abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
13abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
14abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
16abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
18abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
20abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
24abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
30abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
36abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
48abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
60abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
72abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
90abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼body
body
body
body
bodyMetropolis Extra Bold Regular
bodyArial
bodyVerdana
bodyGeorgia

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

Lorem Ipsum Dolor

Etiam porta sem malesuada magna mollis euismod

Donec sed odio dui. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

Pellentesque ornare sem

Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Donec ullamcorper nulla non metus auctor fringilla. Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam id dolor id nibh ultricies vehicula ut id elit.

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Aenean lacinia bibendum nulla sed consectetur.

Nullam quis risus eget urna mollis ornare vel eu leo. Nullam quis risus eget urna mollis ornare vel eu leo. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus auctor fringilla.

Cras mattis consectetur

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean lacinia bibendum nulla sed consectetur. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras mattis consectetur purus sit amet fermentum.

Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu leo. Cras mattis consectetur purus sit amet fermentum.

Language Support

The subset of Metropolis Extra Bold Regular in this kit supports the following languages:
Albanian, Basque, Breton, Chamorro, Danish, Dutch, English, Faroese, Finnish, French, Frisian, Galician, German, Icelandic, Italian, Malagasy, Norwegian, Portuguese, Spanish, Alsatian, Aragonese, Arapaho, Arrernte, Asturian, Aymara, Bislama, Cebuano, Corsican, Fijian, French_creole, Genoese, Gilbertese, Greenlandic, Haitian_creole, Hiligaynon, Hmong, Hopi, Ibanag, Iloko_ilokano, Indonesian, Interglossa_glosa, Interlingua, Irish_gaelic, Jerriais, Lojban, Lombard, Luxembourgeois, Manx, Mohawk, Norfolk_pitcairnese, Occitan, Oromo, Pangasinan, Papiamento, Piedmontese, Potawatomi, Rhaeto-romance, Romansh, Rotokas, Sami_lule, Samoan, Sardinian, Scots_gaelic, Seychelles_creole, Shona, Sicilian, Somali, Southern_ndebele, Swahili, Swati_swazi, Tagalog_filipino_pilipino, Tetum, Tok_pisin, Uyghur_latinized, Volapuk, Walloon, Warlpiri, Xhosa, Yapese, Zulu, Latinbasic, Ubasic, Demo

Glyph Chart

The subset of Metropolis Extra Bold Regular in this kit includes all the glyphs listed below. Unicode entities are included above each glyph to help you insert individual characters into your layout.

&#32;

&#33;

!

&#34;

"

&#35;

#

&#36;

$

&#37;

%

&#38;

&

&#39;

'

&#40;

(

&#41;

)

&#42;

*

&#43;

+

&#44;

,

&#45;

-

&#46;

.

&#47;

/

&#48;

0

&#49;

1

&#50;

2

&#51;

3

&#52;

4

&#53;

5

&#54;

6

&#55;

7

&#56;

8

&#57;

9

&#58;

:

&#59;

;

&#60;

<

&#61;

=

&#62;

>

&#63;

?

&#64;

@

&#65;

A

&#66;

B

&#67;

C

&#68;

D

&#69;

E

&#70;

F

&#71;

G

&#72;

H

&#73;

I

&#74;

J

&#75;

K

&#76;

L

&#77;

M

&#78;

N

&#79;

O

&#80;

P

&#81;

Q

&#82;

R

&#83;

S

&#84;

T

&#85;

U

&#86;

V

&#87;

W

&#88;

X

&#89;

Y

&#90;

Z

&#91;

[

&#92;

\

&#93;

]

&#94;

^

&#95;

_

&#96;

`

&#97;

a

&#98;

b

&#99;

c

&#100;

d

&#101;

e

&#102;

f

&#103;

g

&#104;

h

&#105;

i

&#106;

j

&#107;

k

&#108;

l

&#109;

m

&#110;

n

&#111;

o

&#112;

p

&#113;

q

&#114;

r

&#115;

s

&#116;

t

&#117;

u

&#118;

v

&#119;

w

&#120;

x

&#121;

y

&#122;

z

&#123;

{

&#124;

|

&#125;

}

&#126;

~

&#160;

 

&#161;

¡

&#162;

¢

&#163;

£

&#165;

¥

&#168;

¨

&#173;

­

&#175;

¯

&#180;

´

&#184;

¸

&#191;

¿

&#192;

À

&#193;

Á

&#194;

Â

&#195;

Ã

&#196;

Ä

&#197;

Å

&#198;

Æ

&#199;

Ç

&#200;

È

&#201;

É

&#202;

Ê

&#203;

Ë

&#204;

Ì

&#205;

Í

&#206;

Î

&#207;

Ï

&#208;

Ð

&#209;

Ñ

&#210;

Ò

&#211;

Ó

&#212;

Ô

&#213;

Õ

&#214;

Ö

&#215;

×

&#216;

Ø

&#217;

Ù

&#218;

Ú

&#219;

Û

&#220;

Ü

&#221;

Ý

&#222;

Þ

&#223;

ß

&#224;

à

&#225;

á

&#226;

â

&#227;

ã

&#228;

ä

&#229;

å

&#230;

æ

&#231;

ç

&#232;

è

&#233;

é

&#234;

ê

&#235;

ë

&#236;

ì

&#237;

í

&#238;

î

&#239;

ï

&#240;

ð

&#241;

ñ

&#242;

ò

&#243;

ó

&#244;

ô

&#245;

õ

&#246;

ö

&#247;

÷

&#248;

ø

&#249;

ù

&#250;

ú

&#251;

û

&#252;

ü

&#253;

ý

&#254;

þ

&#255;

ÿ

&#338;

Œ

&#339;

œ

&#376;

Ÿ

&#710;

ˆ

&#732;

˜

&#8192;

 

&#8193;

&#8194;

&#8195;

&#8196;

&#8197;

&#8198;

&#8199;

&#8200;

&#8201;

&#8202;

&#8208;

&#8209;

&#8210;

&#8211;

&#8212;

&#8216;

&#8217;

&#8220;

&#8221;

&#8230;

&#8239;

&#8287;

&#8364;

&#9724;

Installing Webfonts

Webfonts are supported by all major browser platforms but not all in the same way. There are currently four different font formats that must be included in order to target all browsers. This includes TTF, WOFF, EOT and SVG.

1. Upload your webfonts

You must upload your webfont kit to your website. They should be in or near the same directory as your CSS files.

2. Include the webfont stylesheet

A special CSS @font-face declaration helps the various browsers select the appropriate font it needs without causing you a bunch of headaches. Learn more about this syntax by reading the Fontspring blog post about it. The code for it is as follows:

@font-face{ font-family: 'MyWebFont'; src: url('WebFont.eot'); src: url('WebFont.eot?#iefix') format('embedded-opentype'), url('WebFont.woff') format('woff'), url('WebFont.ttf') format('truetype'), url('WebFont.svg#webfont') format('svg'); }

We've already gone ahead and generated the code for you. All you have to do is link to the stylesheet in your HTML, like this:

<link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8" />

3. Modify your own stylesheet

To take advantage of your new fonts, you must tell your stylesheet to use them. Look at the original @font-face declaration above and find the property called "font-family." The name linked there will be what you use to reference the font. Prepend that webfont name to the font stack in the "font-family" property, inside the selector you want to change. For example:

p { font-family: 'WebFont', Arial, sans-serif; }

4. Test

Getting webfonts to work cross-browser can be tricky. Use the information in the sidebar to help you if you find that fonts aren't loading in a particular browser.

================================================ FILE: frontend/src/fonts/metropolis-regular-demo.html ================================================ Metropolis Regular Specimen
AaBb
A​B​C​D​E​F​G​H​I​J​K​L​M​N​O​P​Q​R​S​T​U​V​W​X​Y​Z​a​b​c​d​e​f​g​h​i​j​k​l​m​n​o​p​q​r​s​t​u​v​w​x​y​z​1​2​3​4​5​6​7​8​9​0​&​.​,​?​!​@​(​)​#​$​%​*​+​-​=​:​;
10abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
11abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
12abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
13abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
14abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
16abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
18abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
20abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
24abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
30abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
36abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
48abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
60abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
72abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
90abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼body
body
body
body
bodyMetropolis Regular
bodyArial
bodyVerdana
bodyGeorgia

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

Lorem Ipsum Dolor

Etiam porta sem malesuada magna mollis euismod

Donec sed odio dui. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

Pellentesque ornare sem

Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Donec ullamcorper nulla non metus auctor fringilla. Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam id dolor id nibh ultricies vehicula ut id elit.

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Aenean lacinia bibendum nulla sed consectetur.

Nullam quis risus eget urna mollis ornare vel eu leo. Nullam quis risus eget urna mollis ornare vel eu leo. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus auctor fringilla.

Cras mattis consectetur

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean lacinia bibendum nulla sed consectetur. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras mattis consectetur purus sit amet fermentum.

Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu leo. Cras mattis consectetur purus sit amet fermentum.

Language Support

The subset of Metropolis Regular in this kit supports the following languages:
Albanian, Basque, Breton, Chamorro, Danish, Dutch, English, Faroese, Finnish, French, Frisian, Galician, German, Icelandic, Italian, Malagasy, Norwegian, Portuguese, Spanish, Alsatian, Aragonese, Arapaho, Arrernte, Asturian, Aymara, Bislama, Cebuano, Corsican, Fijian, French_creole, Genoese, Gilbertese, Greenlandic, Haitian_creole, Hiligaynon, Hmong, Hopi, Ibanag, Iloko_ilokano, Indonesian, Interglossa_glosa, Interlingua, Irish_gaelic, Jerriais, Lojban, Lombard, Luxembourgeois, Manx, Mohawk, Norfolk_pitcairnese, Occitan, Oromo, Pangasinan, Papiamento, Piedmontese, Potawatomi, Rhaeto-romance, Romansh, Rotokas, Sami_lule, Samoan, Sardinian, Scots_gaelic, Seychelles_creole, Shona, Sicilian, Somali, Southern_ndebele, Swahili, Swati_swazi, Tagalog_filipino_pilipino, Tetum, Tok_pisin, Uyghur_latinized, Volapuk, Walloon, Warlpiri, Xhosa, Yapese, Zulu, Latinbasic, Ubasic, Demo

Glyph Chart

The subset of Metropolis Regular in this kit includes all the glyphs listed below. Unicode entities are included above each glyph to help you insert individual characters into your layout.

&#32;

&#33;

!

&#34;

"

&#35;

#

&#36;

$

&#37;

%

&#38;

&

&#39;

'

&#40;

(

&#41;

)

&#42;

*

&#43;

+

&#44;

,

&#45;

-

&#46;

.

&#47;

/

&#48;

0

&#49;

1

&#50;

2

&#51;

3

&#52;

4

&#53;

5

&#54;

6

&#55;

7

&#56;

8

&#57;

9

&#58;

:

&#59;

;

&#60;

<

&#61;

=

&#62;

>

&#63;

?

&#64;

@

&#65;

A

&#66;

B

&#67;

C

&#68;

D

&#69;

E

&#70;

F

&#71;

G

&#72;

H

&#73;

I

&#74;

J

&#75;

K

&#76;

L

&#77;

M

&#78;

N

&#79;

O

&#80;

P

&#81;

Q

&#82;

R

&#83;

S

&#84;

T

&#85;

U

&#86;

V

&#87;

W

&#88;

X

&#89;

Y

&#90;

Z

&#91;

[

&#92;

\

&#93;

]

&#94;

^

&#95;

_

&#96;

`

&#97;

a

&#98;

b

&#99;

c

&#100;

d

&#101;

e

&#102;

f

&#103;

g

&#104;

h

&#105;

i

&#106;

j

&#107;

k

&#108;

l

&#109;

m

&#110;

n

&#111;

o

&#112;

p

&#113;

q

&#114;

r

&#115;

s

&#116;

t

&#117;

u

&#118;

v

&#119;

w

&#120;

x

&#121;

y

&#122;

z

&#123;

{

&#124;

|

&#125;

}

&#126;

~

&#160;

 

&#161;

¡

&#162;

¢

&#163;

£

&#165;

¥

&#168;

¨

&#173;

­

&#175;

¯

&#180;

´

&#184;

¸

&#191;

¿

&#192;

À

&#193;

Á

&#194;

Â

&#195;

Ã

&#196;

Ä

&#197;

Å

&#198;

Æ

&#199;

Ç

&#200;

È

&#201;

É

&#202;

Ê

&#203;

Ë

&#204;

Ì

&#205;

Í

&#206;

Î

&#207;

Ï

&#208;

Ð

&#209;

Ñ

&#210;

Ò

&#211;

Ó

&#212;

Ô

&#213;

Õ

&#214;

Ö

&#215;

×

&#216;

Ø

&#217;

Ù

&#218;

Ú

&#219;

Û

&#220;

Ü

&#221;

Ý

&#222;

Þ

&#223;

ß

&#224;

à

&#225;

á

&#226;

â

&#227;

ã

&#228;

ä

&#229;

å

&#230;

æ

&#231;

ç

&#232;

è

&#233;

é

&#234;

ê

&#235;

ë

&#236;

ì

&#237;

í

&#238;

î

&#239;

ï

&#240;

ð

&#241;

ñ

&#242;

ò

&#243;

ó

&#244;

ô

&#245;

õ

&#246;

ö

&#247;

÷

&#248;

ø

&#249;

ù

&#250;

ú

&#251;

û

&#252;

ü

&#253;

ý

&#254;

þ

&#255;

ÿ

&#338;

Œ

&#339;

œ

&#376;

Ÿ

&#710;

ˆ

&#732;

˜

&#8192;

 

&#8193;

&#8194;

&#8195;

&#8196;

&#8197;

&#8198;

&#8199;

&#8200;

&#8201;

&#8202;

&#8208;

&#8209;

&#8210;

&#8211;

&#8212;

&#8216;

&#8217;

&#8220;

&#8221;

&#8230;

&#8239;

&#8287;

&#8364;

&#9724;

Installing Webfonts

Webfonts are supported by all major browser platforms but not all in the same way. There are currently four different font formats that must be included in order to target all browsers. This includes TTF, WOFF, EOT and SVG.

1. Upload your webfonts

You must upload your webfont kit to your website. They should be in or near the same directory as your CSS files.

2. Include the webfont stylesheet

A special CSS @font-face declaration helps the various browsers select the appropriate font it needs without causing you a bunch of headaches. Learn more about this syntax by reading the Fontspring blog post about it. The code for it is as follows:

@font-face{ font-family: 'MyWebFont'; src: url('WebFont.eot'); src: url('WebFont.eot?#iefix') format('embedded-opentype'), url('WebFont.woff') format('woff'), url('WebFont.ttf') format('truetype'), url('WebFont.svg#webfont') format('svg'); }

We've already gone ahead and generated the code for you. All you have to do is link to the stylesheet in your HTML, like this:

<link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8" />

3. Modify your own stylesheet

To take advantage of your new fonts, you must tell your stylesheet to use them. Look at the original @font-face declaration above and find the property called "font-family." The name linked there will be what you use to reference the font. Prepend that webfont name to the font stack in the "font-family" property, inside the selector you want to change. For example:

p { font-family: 'WebFont', Arial, sans-serif; }

4. Test

Getting webfonts to work cross-browser can be tricky. Use the information in the sidebar to help you if you find that fonts aren't loading in a particular browser.

================================================ FILE: frontend/src/fonts/specimen_files/grid_12-825-55-15.css ================================================ /*Notes about grid: Columns: 12 Grid Width: 825px Column Width: 55px Gutter Width: 15px -------------------------------*/ .section {margin-bottom: 18px; } .section:after {content: ".";display: block;height: 0;clear: both;visibility: hidden;} .section {*zoom: 1;} .section .firstcolumn, .section .firstcol {margin-left: 0;} /* Border on left hand side of a column. */ .border { padding-left: 7px; margin-left: 7px; border-left: 1px solid #eee; } /* Border with more whitespace, spans one column. */ .colborder { padding-left: 42px; margin-left: 42px; border-left: 1px solid #eee; } /* The Grid Classes */ .grid1, .grid1_2cols, .grid1_3cols, .grid1_4cols, .grid2, .grid2_3cols, .grid2_4cols, .grid3, .grid3_2cols, .grid3_4cols, .grid4, .grid4_3cols, .grid5, .grid5_2cols, .grid5_3cols, .grid5_4cols, .grid6, .grid6_4cols, .grid7, .grid7_2cols, .grid7_3cols, .grid7_4cols, .grid8, .grid8_3cols, .grid9, .grid9_2cols, .grid9_4cols, .grid10, .grid10_3cols, .grid10_4cols, .grid11, .grid11_2cols, .grid11_3cols, .grid11_4cols, .grid12 {margin-left: 15px;float: left;display: inline; overflow: hidden;} .width1, .grid1, .span-1 {width: 55px;} .width1_2cols,.grid1_2cols {width: 20px;} .width1_3cols,.grid1_3cols {width: 8px;} .width1_4cols,.grid1_4cols {width: 2px;} .input_width1 {width: 49px;} .width2, .grid2, .span-2 {width: 125px;} .width2_3cols,.grid2_3cols {width: 31px;} .width2_4cols,.grid2_4cols {width: 20px;} .input_width2 {width: 119px;} .width3, .grid3, .span-3 {width: 195px;} .width3_2cols,.grid3_2cols {width: 90px;} .width3_4cols,.grid3_4cols {width: 37px;} .input_width3 {width: 189px;} .width4, .grid4, .span-4 {width: 265px;} .width4_3cols,.grid4_3cols {width: 78px;} .input_width4 {width: 259px;} .width5, .grid5, .span-5 {width: 335px;} .width5_2cols,.grid5_2cols {width: 160px;} .width5_3cols,.grid5_3cols {width: 101px;} .width5_4cols,.grid5_4cols {width: 72px;} .input_width5 {width: 329px;} .width6, .grid6, .span-6 {width: 405px;} .width6_4cols,.grid6_4cols {width: 90px;} .input_width6 {width: 399px;} .width7, .grid7, .span-7 {width: 475px;} .width7_2cols,.grid7_2cols {width: 230px;} .width7_3cols,.grid7_3cols {width: 148px;} .width7_4cols,.grid7_4cols {width: 107px;} .input_width7 {width: 469px;} .width8, .grid8, .span-8 {width: 545px;} .width8_3cols,.grid8_3cols {width: 171px;} .input_width8 {width: 539px;} .width9, .grid9, .span-9 {width: 615px;} .width9_2cols,.grid9_2cols {width: 300px;} .width9_4cols,.grid9_4cols {width: 142px;} .input_width9 {width: 609px;} .width10, .grid10, .span-10 {width: 685px;} .width10_3cols,.grid10_3cols {width: 218px;} .width10_4cols,.grid10_4cols {width: 160px;} .input_width10 {width: 679px;} .width11, .grid11, .span-11 {width: 755px;} .width11_2cols,.grid11_2cols {width: 370px;} .width11_3cols,.grid11_3cols {width: 241px;} .width11_4cols,.grid11_4cols {width: 177px;} .input_width11 {width: 749px;} .width12, .grid12, .span-12 {width: 825px;} .input_width12 {width: 819px;} /* Subdivided grid spaces */ .emptycols_left1, .prepend-1 {padding-left: 70px;} .emptycols_right1, .append-1 {padding-right: 70px;} .emptycols_left2, .prepend-2 {padding-left: 140px;} .emptycols_right2, .append-2 {padding-right: 140px;} .emptycols_left3, .prepend-3 {padding-left: 210px;} .emptycols_right3, .append-3 {padding-right: 210px;} .emptycols_left4, .prepend-4 {padding-left: 280px;} .emptycols_right4, .append-4 {padding-right: 280px;} .emptycols_left5, .prepend-5 {padding-left: 350px;} .emptycols_right5, .append-5 {padding-right: 350px;} .emptycols_left6, .prepend-6 {padding-left: 420px;} .emptycols_right6, .append-6 {padding-right: 420px;} .emptycols_left7, .prepend-7 {padding-left: 490px;} .emptycols_right7, .append-7 {padding-right: 490px;} .emptycols_left8, .prepend-8 {padding-left: 560px;} .emptycols_right8, .append-8 {padding-right: 560px;} .emptycols_left9, .prepend-9 {padding-left: 630px;} .emptycols_right9, .append-9 {padding-right: 630px;} .emptycols_left10, .prepend-10 {padding-left: 700px;} .emptycols_right10, .append-10 {padding-right: 700px;} .emptycols_left11, .prepend-11 {padding-left: 770px;} .emptycols_right11, .append-11 {padding-right: 770px;} .pull-1 {margin-left: -70px;} .push-1 {margin-right: -70px;margin-left: 18px;float: right;} .pull-2 {margin-left: -140px;} .push-2 {margin-right: -140px;margin-left: 18px;float: right;} .pull-3 {margin-left: -210px;} .push-3 {margin-right: -210px;margin-left: 18px;float: right;} .pull-4 {margin-left: -280px;} .push-4 {margin-right: -280px;margin-left: 18px;float: right;} ================================================ FILE: frontend/src/fonts/specimen_files/specimen_stylesheet.css ================================================ @import url('grid_12-825-55-15.css'); /* CSS Reset by Eric Meyer - Released under Public Domain http://meyerweb.com/eric/tools/css/reset/ */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td {margin: 0;padding: 0;border: 0;outline: 0; font-size: 100%;vertical-align: baseline; background: transparent;} body {line-height: 1;} ol, ul {list-style: none;} blockquote, q {quotes: none;} blockquote:before, blockquote:after, q:before, q:after {content: ''; content: none;} :focus {outline: 0;} ins {text-decoration: none;} del {text-decoration: line-through;} table {border-collapse: collapse;border-spacing: 0;} body { color: #000; background-color: #dcdcdc; } a { text-decoration: none; color: #1883ba; } h1{ font-size: 32px; font-weight: normal; font-style: normal; margin-bottom: 18px; } h2{ font-size: 18px; } #container { width: 865px; margin: 0px auto; } #header { padding: 20px; font-size: 36px; background-color: #000; color: #fff; } #header span { color: #666; } #main_content { background-color: #fff; padding: 60px 20px 20px; } #footer p { margin: 0; padding-top: 10px; padding-bottom: 50px; color: #333; font: 10px Arial, sans-serif; } .tabs { width: 100%; height: 31px; background-color: #444; } .tabs li { float: left; margin: 0; overflow: hidden; background-color: #444; } .tabs li a { display: block; color: #fff; text-decoration: none; font: bold 11px/11px 'Arial'; text-transform: uppercase; padding: 10px 15px; border-right: 1px solid #fff; } .tabs li a:hover { background-color: #00b3ff; } .tabs li.active a { color: #000; background-color: #fff; } div.huge { font-size: 300px; line-height: 1em; padding: 0; letter-spacing: -.02em; overflow: hidden; } div.glyph_range { font-size: 72px; line-height: 1.1em; } .size10{ font-size: 10px; } .size11{ font-size: 11px; } .size12{ font-size: 12px; } .size13{ font-size: 13px; } .size14{ font-size: 14px; } .size16{ font-size: 16px; } .size18{ font-size: 18px; } .size20{ font-size: 20px; } .size24{ font-size: 24px; } .size30{ font-size: 30px; } .size36{ font-size: 36px; } .size48{ font-size: 48px; } .size60{ font-size: 60px; } .size72{ font-size: 72px; } .size90{ font-size: 90px; } .psample_row1 { height: 120px;} .psample_row1 { height: 120px;} .psample_row2 { height: 160px;} .psample_row3 { height: 160px;} .psample_row4 { height: 160px;} .psample { overflow: hidden; position: relative; } .psample p { line-height: 1.3em; display: block; overflow: hidden; margin: 0; } .psample span { margin-right: .5em; } .white_blend { width: 100%; height: 61px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVkAAAA9CAYAAAAH4BojAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAO1JREFUeNrs3TsKgFAMRUE/eer+NxztxMYuEWQG3ECKwwUF58ycAKixOAGAyAKILAAiCyCyACILgMgCiCyAyAIgsgAiCyCyAIgsgMgCiCwAIgsgsgAiC4DIAogsACIL0CWuZ3UGgLrIhjMA1EV2OAOAJQtgyQLwjOzmDAAiCyCyAIgsQFtkd2cAEFkAkQVAZAHaIns4A4AlC2DJAiCyACILILIAiCzAV5H1dQGAJQsgsgCILIDIAvwisl58AViyAJYsACILILIAIgvAe2T9EhxAZAFEFgCRBeiL7HAGgLrIhjMAWLIAliwAt1OAAQDwygTBulLIlQAAAABJRU5ErkJggg==); position: absolute; bottom: 0; } .black_blend { width: 100%; height: 61px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVkAAAA9CAYAAAAH4BojAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAPJJREFUeNrs3TEKhTAQRVGjibr/9QoxhY2N3Ywo50A28IrLwP9g6b1PAMSYTQAgsgAiC4DIAogsgMgCILIAIgsgsgCILIDIAogsACILILIAIguAyAKILIDIAiCyACILgMgCZCnjLWYAiFGvB0BQZJsZAFyyAC5ZAO6RXc0AILIAIguAyAKkRXYzA4DIAogsACILkBbZ3QwALlkAlywAIgsgsgAiC4DIArwVWf8uAHDJAogsACILILIAv4isH74AXLIALlkARBZAZAFEFoDnyPokOIDIAogsACILkBfZZgaAuMhWMwC4ZAE+p4x3mAEgxinAAJ+XBbPWGkwAAAAAAElFTkSuQmCC); position: absolute; bottom: 0; } .fullreverse { background: #000 !important; color: #fff !important; margin-left: -20px; padding-left: 20px; margin-right: -20px; padding-right: 20px; padding: 20px; margin-bottom:0; } .sample_table td { padding-top: 3px; padding-bottom:5px; padding-left: 5px; vertical-align: middle; line-height: 1.2em; } .sample_table td:first-child { background-color: #eee; text-align: right; padding-right: 5px; padding-left: 0; padding: 5px; font: 11px/12px "Courier New", Courier, mono; } code { white-space: pre; background-color: #eee; display: block; padding: 10px; margin-bottom: 18px; overflow: auto; } .bottom,.last {margin-bottom:0 !important; padding-bottom:0 !important;} .box { padding: 18px; margin-bottom: 18px; background: #eee; } .reverse,.reversed { background: #000 !important;color: #fff !important; border: none !important;} #bodycomparison { position: relative; overflow: hidden; font-size: 72px; height: 90px; white-space: nowrap; } #bodycomparison div{ font-size: 72px; line-height: 90px; display: inline; margin: 0 15px 0 0; padding: 0; } #bodycomparison div span{ font: 10px Arial; position: absolute; left: 0; } #xheight { float: none; position: absolute; color: #d9f3ff; font-size: 72px; line-height: 90px; } .fontbody { position: relative; } .arialbody{ font-family: Arial; position: relative; } .verdanabody{ font-family: Verdana; position: relative; } .georgiabody{ font-family: Georgia; position: relative; } /* @group Layout page */ #layout h1 { font-size: 36px; line-height: 42px; font-weight: normal; font-style: normal; } #layout h2 { font-size: 24px; line-height: 23px; font-weight: normal; font-style: normal; } #layout h3 { font-size: 22px; line-height: 1.4em; margin-top: 1em; font-weight: normal; font-style: normal; } #layout p.byline { font-size: 12px; margin-top: 18px; line-height: 12px; margin-bottom: 0; } #layout p { font-size: 14px; line-height: 21px; margin-bottom: .5em; } #layout p.large{ font-size: 18px; line-height: 26px; } #layout .sidebar p{ font-size: 12px; line-height: 1.4em; } #layout p.caption { font-size: 10px; margin-top: -16px; margin-bottom: 18px; } /* @end */ /* @group Glyphs */ #glyph_chart div{ background-color: #d9f3ff; color: #191E1E; float: left; font-size: 36px; height: 1.2em; line-height: 1.2em; margin-bottom: 1px; margin-right: 1px; text-align: center; width: 1.2em; position: relative; padding: .6em .2em .2em; } #glyph_chart div p { position: absolute; left: 0; top: 0; display: block; text-align: center; font: bold 9px Arial, sans-serif; background-color: #3a768f; width: 100%; color: #fff; padding: 2px 0; } #glyphs h1 { font-family: Arial, sans-serif; } /* @end */ /* @group Installing */ #installing { font: 13px Arial, sans-serif; } #installing p, #glyphs p{ line-height: 1.2em; margin-bottom: 18px; font: 13px Arial, sans-serif; } #installing h3{ font-size: 15px; margin-top: 18px; } /* @end */ #rendering h1 { font-family: Arial, sans-serif; } .render_table td { font: 11px "Courier New", Courier, mono; vertical-align: middle; } ================================================ FILE: frontend/src/fonts.css ================================================ @layer base { /* from https://github.com/spring-io/sagan/blob/b23162035bb467fd6b1c0ed12a787cbc045c470f/sagan-client/src/css/main.css */ @font-face { font-family: "Metropolis"; src: url("./fonts/metropolis-regular-webfont.woff2") format("woff2"), url("./fonts/metropolis-regular-webfont.woff") format("woff"); font-style: normal; font-weight: normal; } @font-face { font-family: "Metropolis"; src: url("./fonts/metropolis-bold-webfont.woff2") format("woff2"), url("./fonts/metropolis-bold-webfont.woff") format("woff"); font-weight: 500; font-style: normal; } @font-face { font-family: "Metropolis"; src: url("./fonts/metropolis-extrabold-webfont.woff2") format("woff2"), url("./fonts/metropolis-extrabold-webfont.woff") format("woff"); font-weight: 600; font-style: normal; } /* open-sans-regular - latin */ @font-face { font-family: "Open Sans"; font-style: normal; font-weight: 400; src: url("./fonts/open-sans-v17-latin/open-sans-v17-latin-regular.eot"); /* IE9 Compat Modes */ src: url("./fonts/open-sans-v17-latin/open-sans-v17-latin-regular.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-regular.woff2") format("woff2"), /* Super Modern Browsers */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-regular.woff") format("woff"), /* Modern Browsers */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-regular.ttf") format("truetype"), /* Safari, Android, iOS */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-regular.svg#OpenSans") format("svg"); /* Legacy iOS */ } /* open-sans-italic - latin */ @font-face { font-family: "Open Sans"; font-style: italic; font-weight: 400; src: url("./fonts/open-sans-v17-latin/open-sans-v17-latin-italic.eot"); /* IE9 Compat Modes */ src: url("./fonts/open-sans-v17-latin/open-sans-v17-latin-italic.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-italic.woff2") format("woff2"), /* Super Modern Browsers */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-italic.woff") format("woff"), /* Modern Browsers */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-italic.ttf") format("truetype"), /* Safari, Android, iOS */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-italic.svg#OpenSans") format("svg"); /* Legacy iOS */ } /* open-sans-600 - latin */ @font-face { font-family: "Open Sans"; font-style: normal; font-weight: 600; src: url("./fonts/open-sans-v17-latin/open-sans-v17-latin-600.eot"); /* IE9 Compat Modes */ src: url("./fonts/open-sans-v17-latin/open-sans-v17-latin-600.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-600.woff2") format("woff2"), /* Super Modern Browsers */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-600.woff") format("woff"), /* Modern Browsers */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-600.ttf") format("truetype"), /* Safari, Android, iOS */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-600.svg#OpenSans") format("svg"); /* Legacy iOS */ } /* open-sans-700 - latin */ @font-face { font-family: "Open Sans"; font-style: normal; font-weight: 700; src: url("./fonts/open-sans-v17-latin/open-sans-v17-latin-700.eot"); /* IE9 Compat Modes */ src: url("./fonts/open-sans-v17-latin/open-sans-v17-latin-700.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-700.woff2") format("woff2"), /* Super Modern Browsers */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-700.woff") format("woff"), /* Modern Browsers */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-700.ttf") format("truetype"), /* Safari, Android, iOS */ url("./fonts/open-sans-v17-latin/open-sans-v17-latin-700.svg#OpenSans") format("svg"); /* Legacy iOS */ } /* work-sans-regular - latin */ @font-face { font-family: "Work Sans"; font-style: normal; font-weight: 400; src: url("./fonts/work-sans-v5-latin/work-sans-v5-latin-regular.eot"); /* IE9 Compat Modes */ src: url("./fonts/work-sans-v5-latin/work-sans-v5-latin-regular.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */ url("./fonts/work-sans-v5-latin/work-sans-v5-latin-regular.woff2") format("woff2"), /* Super Modern Browsers */ url("./fonts/work-sans-v5-latin/work-sans-v5-latin-regular.woff") format("woff"), /* Modern Browsers */ url("./fonts/work-sans-v5-latin/work-sans-v5-latin-regular.ttf") format("truetype"), /* Safari, Android, iOS */ url("./fonts/work-sans-v5-latin/work-sans-v5-latin-regular.svg#WorkSans") format("svg"); /* Legacy iOS */ } /* work-sans-700 - latin */ @font-face { font-family: "Work Sans"; font-style: normal; font-weight: 700; src: url("./fonts/work-sans-v5-latin/work-sans-v5-latin-700.eot"); /* IE9 Compat Modes */ src: url("./fonts/work-sans-v5-latin/work-sans-v5-latin-700.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */ url("./fonts/work-sans-v5-latin/work-sans-v5-latin-700.woff2") format("woff2"), /* Super Modern Browsers */ url("./fonts/work-sans-v5-latin/work-sans-v5-latin-700.woff") format("woff"), /* Modern Browsers */ url("./fonts/work-sans-v5-latin/work-sans-v5-latin-700.ttf") format("truetype"), /* Safari, Android, iOS */ url("./fonts/work-sans-v5-latin/work-sans-v5-latin-700.svg#WorkSans") format("svg"); /* Legacy iOS */ } } @layer base { html { @apply font-open-sans; } html, h1, h2, h3, h4, h5, strong { @apply text-spr-black; } h1 { @apply font-open-sans text-2xl; } p { @apply text-spr-gray-dark; } } ================================================ FILE: frontend/src/graphql-types.txt ================================================ import * as Apollo from "@apollo/client"; import { gql } from "@apollo/client"; export type Maybe = T | null; export type Exact = { [K in keyof T]: T[K]; }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe; }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe; }; /** All built-in and custom scalars, mapped to their actual values */ export interface Scalars { ID: string; String: string; Boolean: boolean; Int: number; Float: number; Date: any; } export interface OwnerFilter { firstName?: Maybe; lastName?: Maybe; address?: Maybe; city?: Maybe; telephone?: Maybe; } export enum OrderType { Asc = "ASC", Desc = "DESC", } export enum OrderField { Id = "id", FirstName = "firstName", LastName = "lastName", Address = "address", City = "city", Telephone = "telephone", } export interface OwnerOrder { field: OrderField; order?: Maybe; } export interface AddPetInput { ownerId: Scalars["Int"]; name: Scalars["String"]; birthDate: Scalars["Date"]; typeId: Scalars["Int"]; } export interface UpdatePetInput { petId: Scalars["Int"]; name?: Maybe; birthDate?: Maybe; typeId?: Maybe; } export interface AddOwnerInput { firstName: Scalars["String"]; lastName: Scalars["String"]; address: Scalars["String"]; city: Scalars["String"]; telephone: Scalars["String"]; } export interface UpdateOwnerInput { ownerId: Scalars["Int"]; firstName?: Maybe; lastName?: Maybe; address?: Maybe; city?: Maybe; telephone?: Maybe; } export interface AddVetInput { firstName: Scalars["String"]; lastName: Scalars["String"]; specialtyIds: Array; } export interface AddVisitInput { petId: Scalars["Int"]; description: Scalars["String"]; date: Scalars["Date"]; vetId?: Maybe; } export interface AddSpecialtyInput { name: Scalars["String"]; } export interface UpdateSpecialtyInput { specialtyId: Scalars["Int"]; name: Scalars["String"]; } export interface RemoveSpecialtyInput { specialtyId: Scalars["Int"]; } export type MeQueryVariables = Exact<{ [key: string]: never }>; export type MeQuery = { me: { username: string; fullname: string } }; export type AddVisitMutationVariables = Exact<{ input: AddVisitInput; }>; export type AddVisitMutation = { addVisit: { visit: { date: any; description: string; id: number } }; }; export type AllVetNamesQueryVariables = Exact<{ [key: string]: never }>; export type AllVetNamesQuery = { vets: Array<{ id: number; firstName: string; lastName: string }>; }; export type FindOwnerByLastNameQueryVariables = Exact<{ page: Scalars["Int"]; lastName?: Maybe; }>; export type FindOwnerByLastNameQuery = { owners: { pageInfo: { hasNext: boolean; hasPrev: boolean; nextPage?: Maybe; prevPage?: Maybe; totalPages: number; currentPage: number; ownersCount: number; }; owners: Array< { pets: Array<{ id: number; name: string }> } & OwnerFieldsFragment >; }; }; export type FindOwnerWithPetsAndVisitsQueryVariables = Exact<{ ownerId: Scalars["Int"]; }>; export type FindOwnerWithPetsAndVisitsQuery = { owner: { pets: Array<{ id: number; name: string; birthDate: any; type: { id: number; name: string }; visits: { visits: Array<{ date: any; description: string; id: number; treatingVet?: Maybe<{ id: number; firstName: string; lastName: string; }>; }>; }; }>; } & OwnerFieldsFragment; }; export type PetVisitsFragment = { id: number; visitConnection: { visits: Array<{ id: number }> }; }; export type OwnerFieldsFragment = { id: number; firstName: string; lastName: string; address: string; city: string; telephone: string; }; export type AddVetMutationVariables = Exact<{ input: AddVetInput; }>; export type AddVetMutation = { result: | { vet?: Maybe<{ id: number; firstName: string; lastName: string; specialties: Array<{ id: number; name: string }>; }>; } | { error: string }; }; export type AllSpecialtiesQueryVariables = Exact<{ [key: string]: never }>; export type AllSpecialtiesQuery = { specialties: Array<{ id: number; name: string }>; }; export type AllVetsQueryVariables = Exact<{ [key: string]: never }>; export type AllVetsQuery = { vets: Array<{ id: number; firstName: string; lastName: string; specialties: Array<{ id: number; name: string }>; }>; }; export type VetAndVisitsQueryVariables = Exact<{ vetId: Scalars["Int"]; }>; export type VetAndVisitsQuery = { vet?: Maybe<{ id: number; firstName: string; lastName: string; visits: { visits: Array<{ date: any; description: string; pet: { id: number; name: string; owner: { id: number; lastName: string; firstName: string }; }; }>; }; }>; }; export const PetVisitsFragmentDoc = gql` fragment PetVisits on Pet { id visitConnection: visits { visits { id } } } `; export const OwnerFieldsFragmentDoc = gql` fragment OwnerFields on Owner { id firstName lastName address city telephone } `; export const MeDocument = gql` query Me { me { username fullname } } `; /** * __useMeQuery__ * * To run a query within a React component, call `useMeQuery` and pass it any options that fit your needs. * When your component renders, `useMeQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useMeQuery({ * variables: { * }, * }); */ export function useMeQuery( baseOptions?: Apollo.QueryHookOptions, ) { return Apollo.useQuery(MeDocument, baseOptions); } export function useMeLazyQuery( baseOptions?: Apollo.LazyQueryHookOptions, ) { return Apollo.useLazyQuery( MeDocument, baseOptions, ); } export type MeQueryHookResult = ReturnType; export type MeLazyQueryHookResult = ReturnType; export type MeQueryResult = Apollo.QueryResult; export const AddVisitDocument = gql` mutation AddVisit($input: AddVisitInput!) { addVisit(input: $input) { visit { date description id } } } `; export type AddVisitMutationFn = Apollo.MutationFunction< AddVisitMutation, AddVisitMutationVariables >; /** * __useAddVisitMutation__ * * To run a mutation, you first call `useAddVisitMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useAddVisitMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [addVisitMutation, { data, loading, error }] = useAddVisitMutation({ * variables: { * input: // value for 'input' * }, * }); */ export function useAddVisitMutation( baseOptions?: Apollo.MutationHookOptions< AddVisitMutation, AddVisitMutationVariables >, ) { return Apollo.useMutation( AddVisitDocument, baseOptions, ); } export type AddVisitMutationHookResult = ReturnType; export type AddVisitMutationResult = Apollo.MutationResult; export type AddVisitMutationOptions = Apollo.BaseMutationOptions< AddVisitMutation, AddVisitMutationVariables >; export const AllVetNamesDocument = gql` query AllVetNames { vets { id firstName lastName } } `; /** * __useAllVetNamesQuery__ * * To run a query within a React component, call `useAllVetNamesQuery` and pass it any options that fit your needs. * When your component renders, `useAllVetNamesQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useAllVetNamesQuery({ * variables: { * }, * }); */ export function useAllVetNamesQuery( baseOptions?: Apollo.QueryHookOptions< AllVetNamesQuery, AllVetNamesQueryVariables >, ) { return Apollo.useQuery( AllVetNamesDocument, baseOptions, ); } export function useAllVetNamesLazyQuery( baseOptions?: Apollo.LazyQueryHookOptions< AllVetNamesQuery, AllVetNamesQueryVariables >, ) { return Apollo.useLazyQuery( AllVetNamesDocument, baseOptions, ); } export type AllVetNamesQueryHookResult = ReturnType; export type AllVetNamesLazyQueryHookResult = ReturnType< typeof useAllVetNamesLazyQuery >; export type AllVetNamesQueryResult = Apollo.QueryResult< AllVetNamesQuery, AllVetNamesQueryVariables >; export const FindOwnerByLastNameDocument = gql` query FindOwnerByLastName($page: Int!, $lastName: String) { owners( page: $page size: 10 filter: { lastName: $lastName } orders: [{ field: lastName }, { field: firstName }] ) { pageInfo { hasNext hasPrev nextPage prevPage totalPages currentPage: pageNumber ownersCount: totalCount } owners { ...OwnerFields pets { id name } } } } ${OwnerFieldsFragmentDoc} `; /** * __useFindOwnerByLastNameQuery__ * * To run a query within a React component, call `useFindOwnerByLastNameQuery` and pass it any options that fit your needs. * When your component renders, `useFindOwnerByLastNameQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useFindOwnerByLastNameQuery({ * variables: { * page: // value for 'page' * lastName: // value for 'lastName' * }, * }); */ export function useFindOwnerByLastNameQuery( baseOptions: Apollo.QueryHookOptions< FindOwnerByLastNameQuery, FindOwnerByLastNameQueryVariables >, ) { return Apollo.useQuery< FindOwnerByLastNameQuery, FindOwnerByLastNameQueryVariables >(FindOwnerByLastNameDocument, baseOptions); } export function useFindOwnerByLastNameLazyQuery( baseOptions?: Apollo.LazyQueryHookOptions< FindOwnerByLastNameQuery, FindOwnerByLastNameQueryVariables >, ) { return Apollo.useLazyQuery< FindOwnerByLastNameQuery, FindOwnerByLastNameQueryVariables >(FindOwnerByLastNameDocument, baseOptions); } export type FindOwnerByLastNameQueryHookResult = ReturnType< typeof useFindOwnerByLastNameQuery >; export type FindOwnerByLastNameLazyQueryHookResult = ReturnType< typeof useFindOwnerByLastNameLazyQuery >; export type FindOwnerByLastNameQueryResult = Apollo.QueryResult< FindOwnerByLastNameQuery, FindOwnerByLastNameQueryVariables >; export const FindOwnerWithPetsAndVisitsDocument = gql` query FindOwnerWithPetsAndVisits($ownerId: Int!) { owner(id: $ownerId) { ...OwnerFields pets { id name birthDate type { id name } visits { visits { date description id treatingVet { id firstName lastName } } } } } } ${OwnerFieldsFragmentDoc} `; /** * __useFindOwnerWithPetsAndVisitsQuery__ * * To run a query within a React component, call `useFindOwnerWithPetsAndVisitsQuery` and pass it any options that fit your needs. * When your component renders, `useFindOwnerWithPetsAndVisitsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useFindOwnerWithPetsAndVisitsQuery({ * variables: { * ownerId: // value for 'ownerId' * }, * }); */ export function useFindOwnerWithPetsAndVisitsQuery( baseOptions: Apollo.QueryHookOptions< FindOwnerWithPetsAndVisitsQuery, FindOwnerWithPetsAndVisitsQueryVariables >, ) { return Apollo.useQuery< FindOwnerWithPetsAndVisitsQuery, FindOwnerWithPetsAndVisitsQueryVariables >(FindOwnerWithPetsAndVisitsDocument, baseOptions); } export function useFindOwnerWithPetsAndVisitsLazyQuery( baseOptions?: Apollo.LazyQueryHookOptions< FindOwnerWithPetsAndVisitsQuery, FindOwnerWithPetsAndVisitsQueryVariables >, ) { return Apollo.useLazyQuery< FindOwnerWithPetsAndVisitsQuery, FindOwnerWithPetsAndVisitsQueryVariables >(FindOwnerWithPetsAndVisitsDocument, baseOptions); } export type FindOwnerWithPetsAndVisitsQueryHookResult = ReturnType< typeof useFindOwnerWithPetsAndVisitsQuery >; export type FindOwnerWithPetsAndVisitsLazyQueryHookResult = ReturnType< typeof useFindOwnerWithPetsAndVisitsLazyQuery >; export type FindOwnerWithPetsAndVisitsQueryResult = Apollo.QueryResult< FindOwnerWithPetsAndVisitsQuery, FindOwnerWithPetsAndVisitsQueryVariables >; export const AddVetDocument = gql` mutation AddVet($input: AddVetInput!) { result: addVet(input: $input) { ... on AddVetSuccessPayload { vet { id firstName lastName specialties { id name } } } ... on AddVetErrorPayload { error } } } `; export type AddVetMutationFn = Apollo.MutationFunction< AddVetMutation, AddVetMutationVariables >; /** * __useAddVetMutation__ * * To run a mutation, you first call `useAddVetMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useAddVetMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [addVetMutation, { data, loading, error }] = useAddVetMutation({ * variables: { * input: // value for 'input' * }, * }); */ export function useAddVetMutation( baseOptions?: Apollo.MutationHookOptions< AddVetMutation, AddVetMutationVariables >, ) { return Apollo.useMutation( AddVetDocument, baseOptions, ); } export type AddVetMutationHookResult = ReturnType; export type AddVetMutationResult = Apollo.MutationResult; export type AddVetMutationOptions = Apollo.BaseMutationOptions< AddVetMutation, AddVetMutationVariables >; export const AllSpecialtiesDocument = gql` query AllSpecialties { specialties { id name } } `; /** * __useAllSpecialtiesQuery__ * * To run a query within a React component, call `useAllSpecialtiesQuery` and pass it any options that fit your needs. * When your component renders, `useAllSpecialtiesQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useAllSpecialtiesQuery({ * variables: { * }, * }); */ export function useAllSpecialtiesQuery( baseOptions?: Apollo.QueryHookOptions< AllSpecialtiesQuery, AllSpecialtiesQueryVariables >, ) { return Apollo.useQuery( AllSpecialtiesDocument, baseOptions, ); } export function useAllSpecialtiesLazyQuery( baseOptions?: Apollo.LazyQueryHookOptions< AllSpecialtiesQuery, AllSpecialtiesQueryVariables >, ) { return Apollo.useLazyQuery( AllSpecialtiesDocument, baseOptions, ); } export type AllSpecialtiesQueryHookResult = ReturnType< typeof useAllSpecialtiesQuery >; export type AllSpecialtiesLazyQueryHookResult = ReturnType< typeof useAllSpecialtiesLazyQuery >; export type AllSpecialtiesQueryResult = Apollo.QueryResult< AllSpecialtiesQuery, AllSpecialtiesQueryVariables >; export const AllVetsDocument = gql` query AllVets { vets { id firstName lastName specialties { id name } } } `; /** * __useAllVetsQuery__ * * To run a query within a React component, call `useAllVetsQuery` and pass it any options that fit your needs. * When your component renders, `useAllVetsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useAllVetsQuery({ * variables: { * }, * }); */ export function useAllVetsQuery( baseOptions?: Apollo.QueryHookOptions, ) { return Apollo.useQuery( AllVetsDocument, baseOptions, ); } export function useAllVetsLazyQuery( baseOptions?: Apollo.LazyQueryHookOptions< AllVetsQuery, AllVetsQueryVariables >, ) { return Apollo.useLazyQuery( AllVetsDocument, baseOptions, ); } export type AllVetsQueryHookResult = ReturnType; export type AllVetsLazyQueryHookResult = ReturnType; export type AllVetsQueryResult = Apollo.QueryResult< AllVetsQuery, AllVetsQueryVariables >; export const VetAndVisitsDocument = gql` query VetAndVisits($vetId: Int!) { vet(id: $vetId) { id firstName lastName visits { visits { date description pet { id name owner { id lastName firstName } } } } } } `; /** * __useVetAndVisitsQuery__ * * To run a query within a React component, call `useVetAndVisitsQuery` and pass it any options that fit your needs. * When your component renders, `useVetAndVisitsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useVetAndVisitsQuery({ * variables: { * vetId: // value for 'vetId' * }, * }); */ export function useVetAndVisitsQuery( baseOptions: Apollo.QueryHookOptions< VetAndVisitsQuery, VetAndVisitsQueryVariables >, ) { return Apollo.useQuery( VetAndVisitsDocument, baseOptions, ); } export function useVetAndVisitsLazyQuery( baseOptions?: Apollo.LazyQueryHookOptions< VetAndVisitsQuery, VetAndVisitsQueryVariables >, ) { return Apollo.useLazyQuery( VetAndVisitsDocument, baseOptions, ); } export type VetAndVisitsQueryHookResult = ReturnType< typeof useVetAndVisitsQuery >; export type VetAndVisitsLazyQueryHookResult = ReturnType< typeof useVetAndVisitsLazyQuery >; export type VetAndVisitsQueryResult = Apollo.QueryResult< VetAndVisitsQuery, VetAndVisitsQueryVariables >; ================================================ FILE: frontend/src/index.css ================================================ @import "./fonts.css"; @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: frontend/src/login/AuthTokenProvider.tsx ================================================ import * as React from "react"; type IAuthContext = { token: string | null; updateToken(token: string | null): void; }; const AuthContext = React.createContext({ token: null, updateToken() {}, }); type AuthContextProviderProps = { children: React.ReactNode; }; export function AuthTokenProvider({ children }: AuthContextProviderProps) { const [token, setToken] = React.useState( undefined, ); React.useEffect(() => { setToken(localStorage.getItem("petclinic.token") || null); }, []); function updateToken(newToken: string | null) { if (!newToken) { localStorage.removeItem("petclinic.token"); } else { localStorage.setItem("petclinic.token", newToken); } setToken(newToken); } return token === undefined ? null : ( {children} ); } export function useAuthToken() { const { token, updateToken } = React.useContext(AuthContext); return [token, updateToken] as const; } ================================================ FILE: frontend/src/login/LoginPage.tsx ================================================ import * as React from "react"; import { useForm } from "react-hook-form"; import { useAuthToken } from "./AuthTokenProvider"; import Button from "@/components/Button"; import ButtonBar from "@/components/ButtonBar"; import Card from "@/components/Card"; import Heading from "@/components/Heading"; import Input from "@/components/Input"; import Label from "@/components/Label"; import { AnonymousPageLayout } from "@/components/PageLayout"; import Table from "@/components/Table"; import { loginApiUrl } from "@/urls"; import { Section } from "@/components/Section.tsx"; type LoginFormData = { username: string; password: string }; type LoginRequestState = { running?: boolean; error?: string }; export default function LoginPage() { const [, updateToken] = useAuthToken(); const { register, handleSubmit, formState: { errors }, } = useForm({}); const [loginRequestState, setLoginRequestState] = React.useState({ running: false }); async function handleLogin({ username, password }: LoginFormData) { setLoginRequestState({ running: true, }); try { const response = await fetch(loginApiUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username, password }), }); if (response.status === 401) { throw new Error("Could not login. Please verify username/password."); } if (!response.ok) { throw new Error("Could not login"); } const result = await response.json(); if (!result.token) { throw new Error("Could not login. Please verify username/password."); } console.log("TOKEN RECEIVED", result.token); updateToken(result.token); } catch (err) { console.error("LOGIN FAILED ================ >>>>>>>>>>>>>>>>> ", err); const msg = err && typeof err === "object" && "message" in err ? String(err.message) : "unknown error"; setLoginRequestState({ error: msg }); } } return (
Login {loginRequestState.error && }
Users

Choose one of the following users for login:

); } ================================================ FILE: frontend/src/login/MeQuery.graphql ================================================ query Me { me { username fullname } } ================================================ FILE: frontend/src/main.tsx ================================================ import * as ReactDOM from "react-dom/client"; import "./index.css"; import { ApolloProvider } from "@apollo/client"; import { AuthTokenProvider } from "./login/AuthTokenProvider"; import { BrowserRouter } from "react-router-dom"; import App from "./App.tsx"; import { createGraphqlClient } from "@/create-graphql-client.ts"; const client = createGraphqlClient(); ReactDOM.createRoot(document.getElementById("root")!).render( , ); ================================================ FILE: frontend/src/owners/AddVisit.graphql ================================================ mutation AddVisit($input: AddVisitInput!) { addVisit(input: $input) { visit { ...DefaultVisit } } } ================================================ FILE: frontend/src/owners/AllVetNames.graphql ================================================ query AllVetNames { vets { edges { node { id firstName lastName } } } } ================================================ FILE: frontend/src/owners/FindOwnerByLastName.graphql ================================================ query FindOwnerByLastName( $after: String $lastName: String $dir: OrderDirection ) { owners( first: 5 after: $after filter: { lastName: $lastName } order: [{ field: lastName, direction: $dir }, { field: firstName }] ) { edges { node { ...OwnerFields pets { id name } } } pageInfo { hasNextPage endCursor } } } ================================================ FILE: frontend/src/owners/FindOwnerWithPetsAndVisits.graphql ================================================ query FindOwnerWithPetsAndVisits($ownerId: Int!) { owner(id: $ownerId) { ...OwnerFields pets { id name birthDate type { id name } visits { visits { ...VisitWithVet } } } } } ================================================ FILE: frontend/src/owners/NewVisitForm.tsx ================================================ import { useAddVisitMutation, useAllVetNamesQuery, } from "@/generated/graphql-types.ts"; import dayjs from "dayjs"; import { useForm } from "react-hook-form"; import Heading from "@/components/Heading.tsx"; import Input from "@/components/Input.tsx"; import Label from "@/components/Label.tsx"; import Select from "@/components/Select.tsx"; import ButtonBar from "@/components/ButtonBar.tsx"; import Button from "@/components/Button.tsx"; import { Section } from "@/components/Section.tsx"; import { filterNull } from "@/utils.ts"; type VisitFormData = { description: string; date: Date; vet?: string; }; const emptyVetOption = { value: -1, label: "", }; type NewVisitFormProps = { onFinish(): void; petId: number; petName: string; }; export default function NewVisitForm({ onFinish, petId, petName, }: NewVisitFormProps) { const { loading: vetsLoading, data: vetsData, error: vetsError, } = useAllVetNamesQuery(); const [addVisit, { called, loading, error }] = useAddVisitMutation(); const { register, handleSubmit, formState: { errors }, } = useForm(); const vetOptions = vetsData ? [ emptyVetOption, ...vetsData.vets.edges .filter(filterNull) .map((v) => v.node) .map((vet) => ({ value: vet.id, label: `${vet.firstName} ${vet.lastName}`, })), ] : null; async function handleAddClick({ description, date, vet }: VisitFormData) { const petclinicDate = dayjs(date).format("YYYY/MM/DD"); const vetId = vet ? parseInt(vet) : emptyVetOption.value; const result = await addVisit({ variables: { input: { petId, description, date: petclinicDate, vetId: vetId === emptyVetOption.value ? null : vetId, }, }, }); if (result.data) { onFinish(); } } return (
Add Visit {vetsError && ( )} {vetsLoading && } {vetOptions && (
Pets and Visits
{data.owner.pets.map((pet) => (
{pet.name} ({pet.type.name}, * {pet.birthDate}) {pet.visits.visits.length ? ( [ visit.date, visit.treatingVet ? ( {visit.treatingVet.firstName} {visit.treatingVet.lastName} ) : ( "" ), visit.description, ])} /> ) : ( No visits yet )} ))} ); } ================================================ FILE: frontend/src/owners/OwnerSearchPage.tsx ================================================ import { useForm } from "react-hook-form"; import Button from "@/components/Button"; import Input from "@/components/Input"; import PageLayout from "@/components/PageLayout"; import Table from "@/components/Table"; import { FindOwnerByLastNameQueryVariables, OrderDirection, useFindOwnerByLastNameLazyQuery, } from "@/generated/graphql-types"; import Link from "@/components/Link.tsx"; import ButtonBar from "@/components/ButtonBar.tsx"; import { useState } from "react"; import { filterNull } from "@/utils.ts"; type FindOwnerFormData = { lastName: string }; export default function OwnersPage() { const [ findOwnersByLastName, { loading, data, error, called, fetchMore, refetch }, ] = useFindOwnerByLastNameLazyQuery(); const { register, handleSubmit, getValues } = useForm({}); const [orderBy, setOrderBy] = useState<"ASC" | "DESC">("ASC"); function handleFindClick() { search(orderBy); } function search(orderBy: "ASC" | "DESC") { const { lastName }: FindOwnerFormData = getValues(); console.log("lastname", lastName); console.log("orderby", orderBy); const dir = orderBy === "ASC" ? OrderDirection.Asc : OrderDirection.Desc; const variables: FindOwnerByLastNameQueryVariables = lastName ? { lastName, after: null, dir } : { lastName: null, after: null, dir }; if (!called) { findOwnersByLastName({ variables }); } else { refetch(variables); } } function handleOrderChange(newOrder: "ASC" | "DESC") { setOrderBy(newOrder); search(newOrder); } function handleFetchMore() { fetchMore({ variables: { after: data?.owners.pageInfo.endCursor, }, }); } let resultTable = null; if (called && !loading && !error && data) { if (data.owners.edges.length === 0) { resultTable =
No owners found
; } else { const values = data.owners.edges .filter(filterNull) .map(({ node: owner }) => [ {owner.lastName}, owner.firstName, owner.address, owner.city, owner.telephone, owner.pets.map((pet) => pet.name).join(", "), ]); resultTable = (
} labels={[ "Last name", "First name", "Address", "City", "Telephone", "Pets", ]} values={values} /> ); } } return (
Find } />
{resultTable}
); } ================================================ FILE: frontend/src/owners/Visit.fragment.graphql ================================================ fragment DefaultVisit on Visit { id date description } ================================================ FILE: frontend/src/owners/VisitWithVet.fragment.graphql ================================================ fragment VisitWithVet on Visit { ...DefaultVisit treatingVet { id firstName lastName } } ================================================ FILE: frontend/src/urls.ts ================================================ const backendHost = ""; export const graphqlApiUrl = `${backendHost}/graphql`; export const loginApiUrl = `${backendHost}/api/login`; function buildWsApiUrl() { if (backendHost === "") { const url = new URL(window.location.href); return `${url.protocol}//${url.host}/graphqlws`; } else { return `${backendHost}/graphqlws`; } } export const graphqlWsApiUrl = buildWsApiUrl() .replace("https", "wss") .replace("http", "ws"); console.log("USING GRAPHQL API URL", graphqlApiUrl); console.log("USING GRAPHQL SUBSCRIPTION URL", graphqlWsApiUrl); console.log("USING LOGIN API URL", loginApiUrl); ================================================ FILE: frontend/src/use-current-user-fullname.tsx ================================================ import { useMeQuery } from "./generated/graphql-types"; export function useCurrentUser() { const { loading, error, data } = useMeQuery(); if (loading || error || !data) { return { username: null, fullname: null }; } return { fullname: data.me.fullname || null, username: data.me.username || null, }; } ================================================ FILE: frontend/src/use-logout.ts ================================================ import { useApolloClient } from "@apollo/client"; import { useAuthToken } from "./login/AuthTokenProvider"; export function useLogout() { const client = useApolloClient(); const [, setAuthToken] = useAuthToken(); return function logout() { setAuthToken(null); client.clearStore(); }; } ================================================ FILE: frontend/src/utils.ts ================================================ export function filterNull(a: A): a is NonNullable { return a !== null; } ================================================ FILE: frontend/src/vets/AddVet.graphql ================================================ mutation AddVet($input: AddVetInput!) { result: addVet(input: $input) { ... on AddVetSuccessPayload { vet { id firstName lastName specialties { id name } } } ... on AddVetErrorPayload { error } } } ================================================ FILE: frontend/src/vets/AddVetForm.tsx ================================================ import Button from "@/components/Button"; import ButtonBar from "@/components/ButtonBar"; import Heading from "@/components/Heading"; import Input from "@/components/Input"; import Label from "@/components/Label"; import Select from "@/components/Select"; import { useAddVetMutation, useAllSpecialtiesQuery, } from "@/generated/graphql-types.ts"; import { useForm } from "react-hook-form"; import { Section } from "@/components/Section.tsx"; type AddVetFormProps = { onFinish(): void; }; type VetFormData = { firstName: string; lastName: string; specialtyIds: number[]; }; export default function AddVetForm({ onFinish }: AddVetFormProps) { const { data: specialtiesData, error: specialtiesError, loading: specialtiesLoading, } = useAllSpecialtiesQuery(); const [addVet, { called, loading, data, error }] = useAddVetMutation({ // Errors in Responses should be retured in 'error' (and not as rejected promise) // https://www.apollographql.com/docs/react/api/react/hoc/#optionserrorpolicy errorPolicy: "all", }); const { register, handleSubmit, formState: { errors }, } = useForm(); const specialtiesOptions = specialtiesData ? [ ...specialtiesData.specialties.map((s) => ({ label: s.name, value: s.id, })), { label: "invalid (use to see errors in response)", value: 666 }, ] : null; async function handleAddClick({ firstName, lastName, specialtyIds, }: VetFormData) { const result = await addVet({ variables: { input: { firstName, lastName, specialtyIds: specialtyIds || [], }, }, }); if (result.data && "vet" in result.data.result) { onFinish(); } } const errorMsg = error ? error.message : data && "error" in data.result ? `Saving failed: ${data.result.error}` : null; return (
Add Veterinary {specialtiesError && ( )} {specialtiesLoading && ( )} {specialtiesOptions && (
r.node) .map(vetRow)} title={"All Veterinarians"} />

You can add a new veterinary here. Note that this is only allowed for users with role ROLE_MANAGER

{vetId && } ); } type VisitsForVetProps = { vetId: number; }; function VisitsForVet({ vetId }: VisitsForVetProps) { const { loading, error, data } = useVetAndVisitsQuery({ variables: { vetId, }, }); if (loading) { return <>Loading...; } if (error) { return <>Error...; } if (!data || !data.vet) { return <>Not found; } return (
Treatments of {data.vet.firstName} {data.vet.lastName}
[ visit.date, visit.pet.name, {visit.pet.owner.firstName} {visit.pet.owner.lastName} , ])} /> ); } ================================================ FILE: frontend/src/vets/VetsPage.tsx ================================================ import * as React from "react"; import { useParams } from "react-router-dom"; import AddVetForm from "./AddVetForm"; import VetsOverview from "./VetsOverview"; import PageLayout from "@/components/PageLayout"; export default function VetsPage() { const { vetId } = useParams<{ vetId?: string }>(); const [formOpen, setFormOpen] = React.useState(false); return ( {formOpen || ( setFormOpen(true)} /> )} {formOpen && setFormOpen(false)} />} ); } ================================================ FILE: frontend/src/vite-env.d.ts ================================================ /// ================================================ FILE: frontend/tailwind.config.js ================================================ import colors from "tailwindcss/colors"; import form from "@tailwindcss/forms"; /** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], darkMode: "media", theme: { colors: { gray: colors.neutral, indigo: colors.indigo, red: colors.rose, yellow: colors.amber, "spr-white": "#fff", "spr-black": "#191e1e", "spr-gray-dark": "#333", "spr-blue": "#086dc3", "spr-green": "#6bb536", "spr-green-dark": "#458b17", "spr-green-light": "#ebf2f2", "spr-red": "#FF3C38", }, fontFamily: { "open-sans": ["Open Sans", "sans-serif"], metro: ["Metropolis", "sans-serif"], helvetica: ["Helvetica", "Arial", "sans-serif"], }, extend: {}, }, variants: { extend: { cursor: ["disabled"], }, }, plugins: [form], }; ================================================ FILE: frontend/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "paths": { "@/*": ["./src/*"] } }, "include": ["src"], "exclude": ["patch-index-html.js"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: frontend/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: frontend/vite.config.ts ================================================ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; // https://vitejs.dev/config/ export default defineConfig({ server: { port: 3080, proxy: { "/api": "http://localhost:9977", "/graphql": "http://localhost:9977", "/graphqlws": { target: "ws://localhost:9977", ws: true, }, }, }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, plugins: [react()], }); ================================================ FILE: graphql.config.yml ================================================ schema: ./backend/src/main/resources/graphql/petclinic.graphqls extensions: endpoints: PetClinic GraphQL Endpoint: url: http://localhost:9977/graphql headers: user-agent: JS GraphQL Authorization: Bearer eyJraWQiOiJjZTc4OTU3Yi03NGM2LTQ0MDEtOTZmYy1hMzQ2YWFiNDE2NGEiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzZWxmIiwic3ViIjoic3VzaSIsImV4cCI6MjAxMzI4MzUzNSwiaWF0IjoxNjk3OTIzNTM1LCJzY29wZSI6Ik1BTkFHRVIifQ.BkYCmxfNUzmVmsA1aD5plfzpIy0wtFBsHGFid187LNz5Jur4LuMV54OsP-V2wjlKgacC-BnC4apLqJskmeE7dIZDq-3iXi94nqURoIsAZf_6lkwfKgQ5FIx_9Kzszm8n5OV8zQnmZjpRUN-v4FJ0-ByMFmIAxjvqNBFdpJ3tfyzLUaPI8LU4kxC_GO_rsFfxjTe5JQtxgFfRMIfe-8z8aVNJd9na0DxT-Mj-Kj6COKX5PVq0Dv6LRsBB5_sktv-IBV7WUWhc_skpd1IU83WhD_aMRpbB7WhM-TTO85sQljOqs22HIo_V-dSzpo4vXz61CppI6mH7wHfDQr5PiHxBzQ introspect: false ================================================ FILE: login.http ================================================ ### PING! GET http://localhost:9977/ping Authorization: Bearer eyJraWQiOiIxMTA4YzcxNS02MGIwLTQ4OGEtYjg4Yy04NGI1Njg1ZDU1MTEiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzZWxmIiwic3ViIjoic3VzaSIsImV4cCI6MTk4NzYwMDQ0MywiaWF0IjoxNjcyMjQwNDQzLCJzY29wZSI6IlJPTEVfTUFOQUdFUiJ9.hNPB-kTUEzTWEeeOOQIbN3QUeB6jFX4NNesuDGNZ9KXt3T_dfEMIBsdOkwrcPqxtkXMzugwKfSx8VV_aqFznO2NlEzlKA7ngDbjWj0T4Ozr9q1E5aVieWzt9QvI7_AoG21e5upea8T7vUO7Dy64YLFcIPL6Gvhgw0zsSyxNxLbyqQBvwpwu5EzwTWmr9plAjGwIiRbnxkbMqK1mdHuh7QemG9am6-ocfPE5SaLXk1w6Y-wWhMQTDS2JtC__tFumLunUUMqm019lqrILQFRGWqRLZRKnRDP2QEyfrSbOdhacCIPY8KOn0Zk7bstgGCyG71soM0HPnpFXthsScZY4_Zg ### LOGIN POST http://localhost:9977/login Content-Type: application/json { "username": "joe", "password": "joe" } ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" else jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: petclinic-graphiql/.eslintrc.cjs ================================================ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', ], ignorePatterns: ['dist', '.eslintrc.cjs'], parser: '@typescript-eslint/parser', plugins: ['react-refresh'], rules: { 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, } ================================================ FILE: petclinic-graphiql/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: petclinic-graphiql/README.md ================================================ # Customized GraphiQL for Spring PetClinic This module contains a customized build of the GraphiQL ui. It shows how you can create your [own, customized GraphiQL](https://docs.spring.io/spring-graphql/reference/graphiql.html#graphiql.custombuild) for your backend that can be integrated in your Spring backend application. Other than the original GraphiQL it has a login screen because the PetClinic GraphQL is not public, and every request needs a JWT to get access to it. ## Tech stack This module uses [Vite](https://vitejs.dev/) with the `react-ts` template. GraphiQL itself is added to it as a [npm module](https://github.com/graphql/graphiql/tree/main/packages/graphiql#using-as-package). ## Install the packages Before you run or build the module, you have to install the node packages: ```bash pnpm install ``` ## Use it locally You can develop, run and test GraphiQL locally without embedding it into the Spring PetClinic GraphQL backend. To do so, use the Vite command: ``` pnpm dev ``` This runs a development server that will connect itself against the running Spring PetClinic Backend. It assumes your backend runs on `http://localhost:9977` (see `urls.ts` and `vite.config.ts` for proxy settings). If you make changes to the code in this module and save your changes, they're immediately picked up from the dev server and should be visible without the need of reloading the GraphiQL page. ## Integration in the Backend When you open the backend on `http://localhost:9977`, there runs also the customized GraphiQL instance, but now served from spring boot. You can update the included version of `petclinic-graphiql` in the `backend` project by running the following steps: - build `petclinic-graphiql` by running `pnpm build` - copy the files from `petclinic-graphiql/dist` to `backend/src/main/resources/ui/graphiql` - Re-build the backend project and re-start it - Opening `http://localhost:9977` should now run your GraphiQL build > When you're working on a bash or zsh, the process can be simplified to: > > ```bash > pnpm build > pnpm copy-to-backend > ``` ================================================ FILE: petclinic-graphiql/index.html ================================================ GraphiQL :: Spring PetClinic
================================================ FILE: petclinic-graphiql/package.json ================================================ { "name": "petclinic-graphiql", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build --base=/graphiql", "copy-to-backend": "rm -rf ../backend/src/main/resources/ui/graphiql && cp -r dist/ ../backend/src/main/resources/ui/graphiql", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "tsc && vite build && vite preview" }, "dependencies": { "@graphiql/toolkit": "^0.9.1", "graphiql": "^3.0.6", "graphql": "^16.8.1", "graphql-ws": "^5.14.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/node": "^20.8.7", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "prettier": "^3.0.3", "typescript": "^5.0.2", "vite": "^4.4.5" } } ================================================ FILE: petclinic-graphiql/pom.xml ================================================ 4.0.0 org.springframework.samples.petclinic-graphql petclinic-graphiql 2.0.0 spring-petclinic-graphiql Customized GraphiQL for Spring Petclinic GraphQL Example ================================================ FILE: petclinic-graphiql/src/App.tsx ================================================ import { createGraphiQLFetcher, Fetcher } from "@graphiql/toolkit"; import { GraphiQL } from "graphiql"; import "graphiql/graphiql.css"; import { useEffect, useMemo, useState } from "react"; import { graphqlApiUrl, graphqlWsApiUrl, pingApiUrl } from "./urls.ts"; import LoginForm from "./LoginForm.tsx"; import { createClient } from "graphql-ws"; type Login = { token: string; username: string }; type LoginVerificationState = | { state: "pending"; } | { state: "verified"; initialLogin: Login | null; }; export default function App() { const [loginVerificationState, setLoginVerificationState] = useState({ state: "pending" }); useEffect(() => { const initialToken = localStorage.getItem("petclinic.graphiql.token"); const initialUsername = localStorage.getItem("petclinic.graphiql.username"); if (!initialUsername || !initialToken) { localStorage.removeItem("petclinic.graphiql.token"); localStorage.removeItem("petclinic.graphiql.username"); setLoginVerificationState({ state: "verified", initialLogin: null }); return; } // call ping endpoint with initial token: // - if it returns HTTP OK, token is valid // - otherwise it's not valid anymore, and user has to login again fetch(pingApiUrl, { headers: { Authorization: `Bearer ${initialToken}`, }, }).then((res) => { if (res.ok) { setLoginVerificationState({ state: "verified", initialLogin: { token: initialToken, username: initialUsername }, }); return; } console.log("Token not valid anymore? Status from ping", res.status); setLoginVerificationState({ state: "verified", initialLogin: null, }); }); }, []); if (loginVerificationState.state === "pending") { return

Verify login...

; } return ; } type AppWithAuthProps = { initialLogin: Login | null; }; function AppWithAuth({ initialLogin }: AppWithAuthProps) { const [currentLogin, setCurrentLogin] = useState<{ token: string; username: string; } | null>(initialLogin); console.log("initialLogin", initialLogin); const [showToken, setShowToken] = useState(false); const fetcher: Fetcher | null = useMemo(() => { if (!currentLogin) { return null; } // When using only the `subscriptionUrl` paran in `createGraphiQLFetcher` // there is a runtime error in the prod build (probalbly bundling related) // so we create ower own client here // The code is inspired by the code that createClient uses internally // node_modules/.pnpm/@graphiql+toolkit@0.9.1_@types+node@20.8.7_graphql-ws@5.14.1_graphql@16.8.1/node_modules/@graphiql/toolkit/src/create-fetcher/createFetcher.ts // getWsFetcher const wsClient = createClient({ url: `${graphqlWsApiUrl}?access_token=${currentLogin.token}`, connectionParams: {}, }); return createGraphiQLFetcher({ url: graphqlApiUrl, wsClient, headers: { Authorization: `Bearer ${currentLogin.token}`, }, }); }, [currentLogin]); if (fetcher) { return ( <>
Logged in as {currentLogin?.username}
{showToken && (
Token
)} ); } return setCurrentLogin(token)} />; } ================================================ FILE: petclinic-graphiql/src/LoginForm.tsx ================================================ import { useState } from "react"; import { loginApiUrl } from "./urls.ts"; type LoginFormProps = { onLogin(login: { username: string; token: string }): void; }; export default function LoginForm({ onLogin }: LoginFormProps) { const [usernamePassword, setUsernamePassword] = useState<{ username: string; password: string; }>({ username: "", password: "", }); function onInputChange(e: React.ChangeEvent) { setLoginMsg(""); setUsernamePassword({ ...usernamePassword, [e.target.name]: e.target.value, }); } const [loginMsg, setLoginMsg] = useState(""); async function login() { setLoginMsg(""); const response = await fetch(loginApiUrl, { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8", }, body: JSON.stringify(usernamePassword), }); if (!response.ok) { localStorage.removeItem("petclinic.graphiql.username"); localStorage.removeItem("petclinic.graphiql.token"); setLoginMsg("Login failed!"); } const result = await response.json(); const token = result.token as string; if (!token) { setLoginMsg("No token in response from server!"); return; } localStorage.setItem( "petclinic.graphiql.username", usernamePassword.username, ); localStorage.setItem("petclinic.graphiql.token", token); onLogin({ token, username: usernamePassword.username }); } return (

Login to Spring PetClinic GraphiQL

{loginMsg}

Choose one of the following users for login:

Username Password Role
susi susi ROLE_MANAGER
joe joe ROLE_USER
); } ================================================ FILE: petclinic-graphiql/src/PetClinicGraphiql.tsx ================================================ import { Fetcher } from "@graphiql/toolkit"; import { GraphiQL } from "graphiql"; type PetClinicGraphiqlProps = { fetcher: Fetcher; }; export default function PetClinicGraphiql({ fetcher }: PetClinicGraphiqlProps) { return ; } ================================================ FILE: petclinic-graphiql/src/index.css ================================================ body { height: 100%; margin: 0; width: 100%; overflow: hidden; } #root { height: 100vh; display: flex; flex-direction: column; } #loginPage { font-family: Arial, Helvetica, sans-serif; max-width: 1200px; margin: 4rem auto; text-align: center; } #loginForm { font-size: 1rem; height: 1.6rem; margin-top: 4rem; } #loginForm input, #loginForm button { font-family: Arial, Helvetica, sans-serif; font-size: 1rem; } #loginForm button, #loginForm label, #loginForm input { margin-left: 1rem; } #loginForm table { text-align: left; border: 1px solid grey; width: 100%; border-collapse: collapse; } #loginForm table tr { border-bottom: 1pt solid grey; } #loginForm table th, #loginForm table td { padding: 0.5rem; } #petclinicToken { white-space: nowrap; font-size: 0.6rem; } .loginInfo, .tokenInfo { font-family: Arial, Helvetica, sans-serif; font-size: 1.2rem; padding: 8px; display: flex; align-items: baseline; gap: 10px; } .loginInfo button, .tokenInfo button { font-family: Arial, Helvetica, sans-serif; font-size: 1.2rem; } .tokenInfo textarea { flex: 1} .tokenInfo--buttons { display: flex; flex-direction: column; } ================================================ FILE: petclinic-graphiql/src/main.tsx ================================================ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")!).render( , ); ================================================ FILE: petclinic-graphiql/src/urls.ts ================================================ const backendHost = ""; export const graphqlApiUrl = `${backendHost}/graphql`; export const loginApiUrl = `${backendHost}/api/login`; export const pingApiUrl = `${backendHost}/ping`; function buildWsApiUrl() { if (backendHost === "") { const url = new URL(window.location.href); return `${url.protocol}//${url.host}/graphqlws`; } else { return `${backendHost}/graphqlws`; } } export const graphqlWsApiUrl = buildWsApiUrl() .replace("https", "wss") .replace("http", "ws"); console.log("USING GRAPHQL API URL", graphqlApiUrl); console.log("USING GRAPHQL SUBSCRIPTION URL", graphqlWsApiUrl); console.log("USING LOGIN API URL", loginApiUrl); ================================================ FILE: petclinic-graphiql/src/vite-env.d.ts ================================================ /// ================================================ FILE: petclinic-graphiql/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"], "exclude": ["patch-index-html.js"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: petclinic-graphiql/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: petclinic-graphiql/vite.config.ts ================================================ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { port: 3081, proxy: { "/api": "http://localhost:9977", "/graphql": "http://localhost:9977", "/graphqlws": { target: "ws://localhost:9977", ws: true, }, }, }, }); ================================================ FILE: pom.xml ================================================ 4.0.0 org.springframework.samples.petclinic-graphql spring-petclinic-graphql 2.0.0 pom petclinic-graphiql backend frontend e2e-tests ================================================ FILE: readme.md ================================================ # PetClinic Sample Application using Spring for GraphQL This PetClinic example uses [Spring for GraphQL](https://github.com/spring-projects/spring-graphql), that is part of Spring Boot [since version 2.7](https://spring.io/blog/2022/05/19/spring-for-graphql-1-0-release). It implements a [GraphQL API](http://graphql.org/) for the PetClinic and provides an example Frontend for the API. Versions currently in use are **Spring Boot 3.2.x** and **Spring for GraphQL 1.2.x**. [![Java CI with Maven](https://github.com/spring-petclinic/spring-petclinic-graphql/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-petclinic/spring-petclinic-graphql/actions/workflows/maven-build.yml) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/spring-petclinic/spring-petclinic-graphql) ## Features Some features that are built in: * [Annotated Controllers](https://docs.spring.io/spring-graphql/reference/controllers.html) (see `graphql/*Controller`-classes, e.g. `SpecialtyController` and `VetController`) * Subscriptions via Websockets (see `VisitController#onNewVisit`) including integration test (see `VisitSubscriptionTest`) and examples below * Own scalar types (See `PetClinicRuntimeWiringConfiguration` and `DateCoercing`) * GraphQL Interfaces (GraphQL Type `Person`) and Unions (GraphQL Type `AddVetPayload`), see class `PetClinicRuntimeWiringConfiguration` * Security: the `/graphql` http and WebSocket endpoints are secured and can only be accessed using a JWT token. More fine grained security is implemented using `@PreAuthorize` (see `VetService`) * Example: `addVet` mutation is only allowed for users with `ROLE_MANAGER` * Pagination, Filtering and Sorting of results: Implemented using the [Pagination support](https://docs.spring.io/spring-graphql/reference/request-execution.html#execution.pagination) of Spring GraphQL, see `OwnerController`. The result of the `owners` field is a `Connection` object defined in the [Cursor Connection specification](https://relay.dev/graphql/connections.htm). * The Apollo client in the React frontend uses the [`relayStylePagination`](https://www.apollographql.com/docs/react/pagination/cursor-based/#relay-style-cursor-pagination) helper function to automatically manage the Client-side cache with new objects read using the `after` cursor query argument. (See `OwnerSearchPage.tsx`) * Custom GraphiQL Build, that has its own login screen, since the PetClinic GraphQL is only accessible with a Token * see project `petclinic-graphiql` * Tests: See `test` folder for typical GraphQL endpoint tests, including tests for security * The tests are using [Spring Boot TestContains support](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.testcontainers) to run the required Postgres database. * End-to-end browser tests: see `e2e-tests` folder for some [Playwright](https://playwright.dev/) based end-to-end tests that test the running application in a real browser. Read description below how to run the tests. * GitHub action workflow: * builds and tests the backend * starts the backend including database with docker-compose to run the end-to-end-tests * (see `.github/workflows/build-app.yml`) # Running the sample application You can run the sample application with two ways: 1. The easiest way: run it pre-configured in cloud IDE [GitPod](https://www.gitpod.io/) 2. Run it locally ## Run it in GitPod To run the application (backend, GraphiQL and React frontend) in GitPod, simply click on the "Open in GitPod" button at the top of this README. - Note that you need a (free) GitPod account. - And please make sure that you allow your browser opening new tabs/windows from gitpod.io! After clicking on the GitPod button, GitPod creates a new workspace including an Editor for you, builds the application and starts backend and frontend. That might take some time! When backend and frontend are running, GitPod opens two new browser tabs, one with GraphiQL and one with the PetClinic backend. For login options, see below "Accessing the GraphQL API" Note that the workspace is your personal workspace, you can make changes, save files, re-open the workspace at any time and you can even create git commits and pull requests from it. For more information see GitPod documentation. In the GitPod editor you can make changes to the app, and after saving the app will be recompiled and redeployed automatically. ![SpringBoot PetClinic in GitPod Workspace](gitpod.png) ### Using IntelliJ with GitPod Recently GitPod has added support for JetBrain IDEs like IntelliJ. While this support is currenty beta only, you can try it and open the PetClinic in IntelliJ. Note that in this scenario you're still working on a complete, ready-to-use workspace in the cloud. Only the IntelliJ _UI_ runs locally at your maching. Please read the [setup instructions here](https://www.gitpod.io/docs/ides-and-editors/intellij). ![SpringBoot PetClinic in GitPod IntelliJ Workspace](screenshot-gitpod-intellij.png) ## Running locally The server is implemented in the `backend` folder and can be started either from your IDE (`org.springframework.samples.petclinic.PetClinicApplication`) or using maven from the root folder of the repository: ``` ./mvnw spring-boot:run -pl backend ``` Note: the server runs on port **9977**, so make sure, this port is available. - Note: you need to have docker installed. `docker-compose` needs to be in your path - On startup the server uses [Spring Boot docker compose support](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.docker-compose) to run the required postgres database ## Running the frontend While you can access the whole GraphQL API from GraphiQL this demo application also contains a modified version of the classic PetClinic UI. Compared to the original client this client is built as a Single-Page-Application using **React** and **Apollo GraphQL** and has slightly different features to make it a more realistic use-case for GraphQL. You can install and start the frontend by using [pnpm](https://pnpm.io/): ``` cd ./frontend pnpm install pnpm codegen pnpm start ``` The running frontend can be accessed on [http://localhost:3080](http://localhost:3080). For valid users to login, see list above. ![SpringBoot PetClinic, React Frontend](petclinic-ui.png) # Deployment There are two scenarios: local development environment and "production" environment **Local development** In local development: - the backend runs on http://localhost:9977 (GraphQL API and graphiql) - the Vite development server for the frontend runs on http://localhost:3080 - the postgres database is started automatically by Spring Boot using the `docker-compose.yml` file in the root folder In this scenario, the vite server acts also as a reverse proxy, that proxies all requests to `/api`, `/graphql` and `/graphqlws` to the backend server (localhost:9977). The proxy is configured in `frontend/vite.config.ts` If you like you can run the customized graphiql with its own Vite development server (using `pnpm dev` in `petclinic-graphiql`) that runs on http://localhost:3081. This is handy if you want to make changes to GraphiQL. **"Production" environment** In this setup, the backend and frontend process run as docker containers using a docker-compose setup that is described in `docker-compose-petclinic.yml`: - the backend port is exposed as http://localhost:3091 (GraphQL API and graphiql) - the nginx for the frontend is exposed as http://localhost:3090 - the postgres database is not exposed outside the container Here the nginx acts as the proxy to the backend. You can build the docker images for backend and frontend using the `build-local.sh` scripts. Also, these images are build during the GitHub workflow. # Accessing the GraphQL API You can access the GraphQL API via the included customized version of GraphiQL. The included GraphiQL adds support for login to the original GraphiQL. You can use the following users for login: * **joe/joe**: Regular user * **susi/susi**: has Manager Role and is allowed to execute the `createVet` Mutation After starting the server, GraphiQL runs on [http://localhost:9977](http://localhost:9977) ## Sample Queries Here you can find some sample queries that you can copy+paste and run in GraphiQL. Feel free to explore and try more 😊. **Query** find first 2 owners whose lastname starts with "D" and their pets, order by lastname and firstname ```graphql query { owners( first: 2 filter: { lastName: "d" } order: [{ field: lastName }, { field: firstName, direction: DESC }] ) { edges { cursor node { id firstName lastName pets { id name } } } pageInfo { hasNextPage endCursor } } } ``` The following query should return two items, but we have more then two owners in the database staring with a `d`. Thus, the `pageInfo.hasNextPage`-field returned in the result of the query above is `true` and the `endCursor` points to the last object returned. Using this cursor as `after` we can receive the next batch of Owners: ```graphql query { owners( first: 2 after: "T18y" filter: { lastName: "d" } order: [{ field: lastName }, { field: firstName, direction: DESC }] ) { edges { cursor node { id firstName lastName pets { id name } } } pageInfo { hasNextPage endCursor } } } ``` Add a new Visit using a **mutation** (can be done with user `joe` and `susi`) and read id and pet of the new created visit: ```graphql mutation { addVisit(input:{ petId:3, description:"Check teeth", date:"2022/03/30", vetId:1 }) { newVisit:visit { id pet { id name birthDate } } } } ``` Add a new veterinarian. This is only allowed for users with `ROLE_MANAGER` and that is `susi`: ```graphql mutation { addVet(input: { firstName: "Dagmar", lastName: "Smith", specialtyIds: [1, 3]}) { ... on AddVetSuccessPayload { newVet: vet { id specialties { id name } } } ... on AddVetErrorPayload { error } } } ``` Listen for new visits using a **Subscription** This mutation selects the treating veterinarian of the new created Visit and the pet that will be visiting. You can either create a new Visit using the mutation above or using the frontend application. ```graphql subscription { onNewVisit { description treatingVet { id firstName lastName } pet { id name } } } ``` **Note:** In the frontend application, you can open an Owner an see all its pets including their visits. If you add a new visit to one of the pets, in all other browser windows that have the Owner page with that Owner open, the new visit should be added to the table automatically, because the `OwnerPage` React component uses a subscription to update the table contents in the background. ![SpringBoot PetClinic, GraphiQL](graphiql.png) ## Customized GraphiQL The backend includes a Spring Petclinic-specific customized version of GraphiQL. Compared to GraphiQL that is embedded by default, the customized version has a login form so that it can send JWT Authentication header with each request to the GraphQL backend. Please see the subproject `petclinic-graphiql` for more information. # End-to-end tests Inside the folder `e2e-tests` you find some Playwright-based end-to-end tests. To run the test, please make sure, the backend and the frontend processes are running, as described above. Then install playwright and all its dependencies including the required browsers by running ``` cd e2e-tests pnpm install ``` Then you can use `pnpm` to start the test: * `pnpm test` will execute all tests in headless mode in all three configured browsers (Chrome, Firefox, Safari) * `pnpm test:ui` opens the tests in [Playwright's UI mode](https://playwright.dev/docs/test-ui-mode) * From the started Playwright UI you can individually select which test to run in which browser * You can also debug the tests from there * `pnpm test:headed`: runs the tests in a headed (i.e. visible) browser (by default Chrome). * `pnpm test:docker-compose` runs the test agains the docker-compose-based setup (localhost:3090/localhost:3091) ## Running, debugging and developing the tests For writing and running Playwright tests I prefer VS code with the [Playwright extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) But if you want to develop and run the tests in IntelliJ, you can install the [Test Automation Plug-in](https://plugins.jetbrains.com/plugin/20175-test-automation) by Jetbrains. # Contributing If you like to help and contribute you're more than welcome! Please open [an issue](https://github.com/spring-petclinic/spring-petclinic-graphql/issues) or a [Pull Request](https://github.com/spring-petclinic/spring-petclinic-graphql/pulls) Initial implementation of this GraphQL-based PetClinic example: [Nils Hartmann](https://nilshartmann.net), [Twitter](https://twitter.com/nilshartmann) ================================================ FILE: talk/curl-demo.sh ================================================ #! /bin/bash curl -X POST \ -H "Content-Type: application/json" \ -d '{"query": "{ pets { name } }"}' \ http://localhost:9977/graphql ================================================ FILE: talk/graphql-introduction.html ================================================ GraphQL Introduction

GraphQL

A query language for your API

@nilshartmann

GraphQL

A query language for your API

"GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data"

http://graphql.org

Invented by Facebook for it's mobile News Feed 2012

Open Source since 2015

Who uses GraphQL?

Facebook (obviously)...

Twitter

GitHub

New York Times

Example: Spring PetClinic

GraphQL in practice

https://github.com/spring-petclinic/spring-petclinic-graphql

Live on Localhost

GraphiQL

GraphQL concepts

Query language

query {
  owner(id: 1) {
    firstName
    lastName
  }
}
            
  • Structured language for accessing data from API
  • Looks similar to JSON (but isn't)
  • Request fields from (nested) objects
  • Fields can have parameters (like params in functions)

The Query Language

Operations

  • A Request consists of an Operation:
    • Query: A read request
    • Mutation: Request for modifying data
  • An Operation can contain parameters (ownerId: 1) and can return a value

Subscriptions

(New in Spec since May 2017)

  • Client can subscribe to changes
  • On changes the Client will be notified (Push)

Type system

Defines the API (its Objects and Operations)

  • Used by the GraphQL runtime to validate requests and responses
  • Scalar Types, Objects, Interfaces, Lists, Enums, ...
  • Invalid Request are rejected by the runtime (no need to care about in own code)
  • Basis for various tooling (Syntax Checking, Code Completion, API Browser, Documentation, ...)

Runtime

Server that executes GraphQL requests

  • Libs for several languages exists
  • Reference implementation: JavaScript
  • Parses and validates the Request
  • Checks and serializes the Response
  • Actual retrieving of the requested data has to be implemented by the application itself

GraphQL Server

Java implementations

graphql-java-tools

graphql-java

graphql-spring-boot

Schema definition using an IDL

(IDL based on JS reference implementation)


# The Query Root type
type Query {
  # Return a List of all pets that have been registered in the PetClinic
  pets: [Pet!]!

  # Return the Pet with the specified id
  pet(id: Int!): Pet!
}

type Pet {
  id: Int!
  name: String!
  type: PetType!
}
            

Resolver

  • Fetch data for a field
  • There must be one Root resolver (for your query type)
  • Properties on beans are accessed using get-methods
  • You can define own resolvers for properties too

Example: Root Resolver


public class Query implements GraphQLQueryResolver {

    private final PetRepository petRepository;

    public Query(PetRepository petRepository) {
        this.petRepository = petRepository;
    }

    public Pet pet(int id) {
        return petRepository.findById(id);
    }

    public List<Pet> pets() {
        return petRepository.findAll();
    }
}

        

Example: Resolver for a type


@Component
public class
PetResolver implements GraphQLResolver<Pet> {
    public VisitConnection visits(Pet pet) {
        return new VisitConnection(pet.getVisits());
    }

    // all other fields that are exposed via the IDL
    // are accessed directly from the Pet class
}
        

Example: Mutation definition


type Mutation {
  # Add a new Pet
  addPet(input: AddPetInput!): AddPetPayload!
}

# The Input for addPet Mutation
input AddPetInput {
  ownerId: Int!
  name: String!
  birthDate: Date!
  typeId: Int!
}

# Return value for addPet Mutation
type AddPetPayload {
  pet: Pet!
}

        

Example: Mutation Resolver


@Component
public class Mutation implements GraphQLMutationResolver {
    private final PetRepository petRepository;
    . . .

    public Mutation(. . .) { . . . }

    public AddPetPayload addPet(AddPetInput addPetInput) {
        final Owner owner = ownerRepository.findById(addPetInput.getOwnerId());
        final PetType type = petTypeRepository.findById(addPetInput.getTypeId());

        Pet pet = new Pet();
        pet.setName(addPetInput.getName());
        pet.setType(type);
        pet.setBirthDate(addPetInput.getBirthDate());
        pet.setOwner(owner);
        petRepository.save(pet);

        return new AddPetPayload(pet);
    }
}
        

GraphQL Client

Example: Plain HTTP/JSON


curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ pets { name } }"}' \
  http://localhost:9977/graphql
        

Result:


{"data":
  {"pets":
    [
      {"name":"Leo"},{"name":"Basil"}, . . .
    ]
  }
}
        

JavaScript implementations

Reference implementation

Apollo (for various Frameworks)

React Apollo (used here)

graphql HOC

Apollo provides a graphql HOC that:

  • Sends a Request to the server
  • Informs the client about the request status (e.g. loading)
  • Passes the result to the wrapped component
  • Does caching

Example: A PetList component


// graphql HOC passes in a data object
const PetList = ({data}) =>
  data.loading ?
     // loading is set to true by Apollo
     // while request is pending
     <div>Loading...</div>
  :
    // otherwise we have the requested data here
    {data.owners.map(owner => ...) }
;
        

Example: The graphql HOC


const PetList = . . .;
const ownersQuery = gql`
  query ownerList {
    owners {
      lastName
      firstName
  }
}
`
export default graphql(ownersQuery)(PetList)
        

Why GraphQL?

Perfect Fetching

  • Returns exactly what's needed
  • No under- or over fetching anymore
    • All needed data with one Request
    • Not more data as needed
  • The shape of the result is specified in the query

API Description using a Type System

  • Good development experience thanks to type system
  • Validation by GraphQL Runtime
  • Errors can be detected at development time

Flexibility and decoupling

  • Client can raise individual Requests as needed
  • No need to enhance API on serverside for new use cases (just query for other data)
  • Enhancing/Changing the API without versioning possible (deprecations exists)

Simplification

  • There is only one endpoint
  • APIs all look the same (query language is standardized)
  • Communication via JSON
  • What fields are actually used can easily be tracked (and controlled) (eg for data protection!)
================================================ FILE: talk/lib/jquery-2.2.4.js ================================================ /*! * jQuery JavaScript Library v2.2.4 * http://jquery.com/ * * Includes Sizzle.js * http://sizzlejs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * http://jquery.org/license * * Date: 2016-05-20T17:23Z */ (function( global, factory ) { if ( typeof module === "object" && typeof module.exports === "object" ) { // For CommonJS and CommonJS-like environments where a proper `window` // is present, execute the factory and get jQuery. // For environments that do not have a `window` with a `document` // (such as Node.js), expose a factory as module.exports. // This accentuates the need for the creation of a real `window`. // e.g. var jQuery = require("jquery")(window); // See ticket #14549 for more info. module.exports = global.document ? factory( global, true ) : function( w ) { if ( !w.document ) { throw new Error( "jQuery requires a window with a document" ); } return factory( w ); }; } else { factory( global ); } // Pass this if window is not defined yet }(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { // Support: Firefox 18+ // Can't be in strict mode, several libs including ASP.NET trace // the stack via arguments.caller.callee and Firefox dies if // you try to trace through "use strict" call chains. (#13335) //"use strict"; var arr = []; var document = window.document; var slice = arr.slice; var concat = arr.concat; var push = arr.push; var indexOf = arr.indexOf; var class2type = {}; var toString = class2type.toString; var hasOwn = class2type.hasOwnProperty; var support = {}; var version = "2.2.4", // Define a local copy of jQuery jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQuery.fn.init( selector, context ); }, // Support: Android<4.1 // Make sure we trim BOM and NBSP rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, // Matches dashed string for camelizing rmsPrefix = /^-ms-/, rdashAlpha = /-([\da-z])/gi, // Used by jQuery.camelCase as callback to replace() fcamelCase = function( all, letter ) { return letter.toUpperCase(); }; jQuery.fn = jQuery.prototype = { // The current version of jQuery being used jquery: version, constructor: jQuery, // Start with an empty selector selector: "", // The default length of a jQuery object is 0 length: 0, toArray: function() { return slice.call( this ); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { return num != null ? // Return just the one element from the set ( num < 0 ? this[ num + this.length ] : this[ num ] ) : // Return all the elements in a clean array slice.call( this ); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) pushStack: function( elems ) { // Build a new jQuery matched element set var ret = jQuery.merge( this.constructor(), elems ); // Add the old object onto the stack (as a reference) ret.prevObject = this; ret.context = this.context; // Return the newly-formed element set return ret; }, // Execute a callback for every element in the matched set. each: function( callback ) { return jQuery.each( this, callback ); }, map: function( callback ) { return this.pushStack( jQuery.map( this, function( elem, i ) { return callback.call( elem, i, elem ); } ) ); }, slice: function() { return this.pushStack( slice.apply( this, arguments ) ); }, first: function() { return this.eq( 0 ); }, last: function() { return this.eq( -1 ); }, eq: function( i ) { var len = this.length, j = +i + ( i < 0 ? len : 0 ); return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); }, end: function() { return this.prevObject || this.constructor(); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. push: push, sort: arr.sort, splice: arr.splice }; jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[ 0 ] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; // Skip the boolean and the target target = arguments[ i ] || {}; i++; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { target = {}; } // Extend jQuery itself if only one argument is passed if ( i === length ) { target = this; i--; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( ( options = arguments[ i ] ) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject( copy ) || ( copyIsArray = jQuery.isArray( copy ) ) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray( src ) ? src : []; } else { clone = src && jQuery.isPlainObject( src ) ? src : {}; } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; }; jQuery.extend( { // Unique for each copy of jQuery on the page expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), // Assume jQuery is ready without the ready module isReady: true, error: function( msg ) { throw new Error( msg ); }, noop: function() {}, isFunction: function( obj ) { return jQuery.type( obj ) === "function"; }, isArray: Array.isArray, isWindow: function( obj ) { return obj != null && obj === obj.window; }, isNumeric: function( obj ) { // parseFloat NaNs numeric-cast false positives (null|true|false|"") // ...but misinterprets leading-number strings, particularly hex literals ("0x...") // subtraction forces infinities to NaN // adding 1 corrects loss of precision from parseFloat (#15100) var realStringObj = obj && obj.toString(); return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0; }, isPlainObject: function( obj ) { var key; // Not plain objects: // - Any object or value whose internal [[Class]] property is not "[object Object]" // - DOM nodes // - window if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { return false; } // Not own constructor property must be Object if ( obj.constructor && !hasOwn.call( obj, "constructor" ) && !hasOwn.call( obj.constructor.prototype || {}, "isPrototypeOf" ) ) { return false; } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own for ( key in obj ) {} return key === undefined || hasOwn.call( obj, key ); }, isEmptyObject: function( obj ) { var name; for ( name in obj ) { return false; } return true; }, type: function( obj ) { if ( obj == null ) { return obj + ""; } // Support: Android<4.0, iOS<6 (functionish RegExp) return typeof obj === "object" || typeof obj === "function" ? class2type[ toString.call( obj ) ] || "object" : typeof obj; }, // Evaluates a script in a global context globalEval: function( code ) { var script, indirect = eval; code = jQuery.trim( code ); if ( code ) { // If the code includes a valid, prologue position // strict mode pragma, execute code by injecting a // script tag into the document. if ( code.indexOf( "use strict" ) === 1 ) { script = document.createElement( "script" ); script.text = code; document.head.appendChild( script ).parentNode.removeChild( script ); } else { // Otherwise, avoid the DOM node creation, insertion // and removal by using an indirect global eval indirect( code ); } } }, // Convert dashed to camelCase; used by the css and data modules // Support: IE9-11+ // Microsoft forgot to hump their vendor prefix (#9572) camelCase: function( string ) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); }, nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); }, each: function( obj, callback ) { var length, i = 0; if ( isArrayLike( obj ) ) { length = obj.length; for ( ; i < length; i++ ) { if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { break; } } } else { for ( i in obj ) { if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { break; } } } return obj; }, // Support: Android<4.1 trim: function( text ) { return text == null ? "" : ( text + "" ).replace( rtrim, "" ); }, // results is for internal usage only makeArray: function( arr, results ) { var ret = results || []; if ( arr != null ) { if ( isArrayLike( Object( arr ) ) ) { jQuery.merge( ret, typeof arr === "string" ? [ arr ] : arr ); } else { push.call( ret, arr ); } } return ret; }, inArray: function( elem, arr, i ) { return arr == null ? -1 : indexOf.call( arr, elem, i ); }, merge: function( first, second ) { var len = +second.length, j = 0, i = first.length; for ( ; j < len; j++ ) { first[ i++ ] = second[ j ]; } first.length = i; return first; }, grep: function( elems, callback, invert ) { var callbackInverse, matches = [], i = 0, length = elems.length, callbackExpect = !invert; // Go through the array, only saving the items // that pass the validator function for ( ; i < length; i++ ) { callbackInverse = !callback( elems[ i ], i ); if ( callbackInverse !== callbackExpect ) { matches.push( elems[ i ] ); } } return matches; }, // arg is for internal usage only map: function( elems, callback, arg ) { var length, value, i = 0, ret = []; // Go through the array, translating each of the items to their new values if ( isArrayLike( elems ) ) { length = elems.length; for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { ret.push( value ); } } // Go through every key on the object, } else { for ( i in elems ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { ret.push( value ); } } } // Flatten any nested arrays return concat.apply( [], ret ); }, // A global GUID counter for objects guid: 1, // Bind a function to a context, optionally partially applying any // arguments. proxy: function( fn, context ) { var tmp, args, proxy; if ( typeof context === "string" ) { tmp = fn[ context ]; context = fn; fn = tmp; } // Quick check to determine if target is callable, in the spec // this throws a TypeError, but we will just return undefined. if ( !jQuery.isFunction( fn ) ) { return undefined; } // Simulated bind args = slice.call( arguments, 2 ); proxy = function() { return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed proxy.guid = fn.guid = fn.guid || jQuery.guid++; return proxy; }, now: Date.now, // jQuery.support is not used in Core but other projects attach their // properties to it so it needs to exist. support: support } ); // JSHint would error on this code due to the Symbol not being defined in ES5. // Defining this global in .jshintrc would create a danger of using the global // unguarded in another place, it seems safer to just disable JSHint for these // three lines. /* jshint ignore: start */ if ( typeof Symbol === "function" ) { jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; } /* jshint ignore: end */ // Populate the class2type map jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), function( i, name ) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); } ); function isArrayLike( obj ) { // Support: iOS 8.2 (not reproducible in simulator) // `in` check used to prevent JIT error (gh-2145) // hasOwn isn't used here due to false negatives // regarding Nodelist length in IE var length = !!obj && "length" in obj && obj.length, type = jQuery.type( obj ); if ( type === "function" || jQuery.isWindow( obj ) ) { return false; } return type === "array" || length === 0 || typeof length === "number" && length > 0 && ( length - 1 ) in obj; } var Sizzle = /*! * Sizzle CSS Selector Engine v2.2.1 * http://sizzlejs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * http://jquery.org/license * * Date: 2015-10-17 */ (function( window ) { var i, support, Expr, getText, isXML, tokenize, compile, select, outermostContext, sortInput, hasDuplicate, // Local document vars setDocument, document, docElem, documentIsHTML, rbuggyQSA, rbuggyMatches, matches, contains, // Instance-specific data expando = "sizzle" + 1 * new Date(), preferredDoc = window.document, dirruns = 0, done = 0, classCache = createCache(), tokenCache = createCache(), compilerCache = createCache(), sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; } return 0; }, // General-purpose constants MAX_NEGATIVE = 1 << 31, // Instance methods hasOwn = ({}).hasOwnProperty, arr = [], pop = arr.pop, push_native = arr.push, push = arr.push, slice = arr.slice, // Use a stripped-down indexOf as it's faster than native // http://jsperf.com/thor-indexof-vs-for/5 indexOf = function( list, elem ) { var i = 0, len = list.length; for ( ; i < len; i++ ) { if ( list[i] === elem ) { return i; } } return -1; }, booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", // Regular expressions // http://www.w3.org/TR/css3-selectors/#whitespace whitespace = "[\\x20\\t\\r\\n\\f]", // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + // Operator (capture 2) "*([*^$|!~]?=)" + whitespace + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + "*\\]", pseudos = ":(" + identifier + ")(?:\\((" + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: // 1. quoted (capture 3; capture 4 or capture 5) "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + // 2. simple (capture 6) "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + // 3. anything else (capture 2) ".*" + ")\\)|)", // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter rwhitespace = new RegExp( whitespace + "+", "g" ), rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), matchExpr = { "ID": new RegExp( "^#(" + identifier + ")" ), "CLASS": new RegExp( "^\\.(" + identifier + ")" ), "TAG": new RegExp( "^(" + identifier + "|[*])" ), "ATTR": new RegExp( "^" + attributes ), "PSEUDO": new RegExp( "^" + pseudos ), "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), // For use in libraries implementing .is() // We use this for POS matching in `select` "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) }, rinputs = /^(?:input|select|textarea|button)$/i, rheader = /^h\d$/i, rnative = /^[^{]+\{\s*\[native \w/, // Easily-parseable/retrievable ID or TAG or CLASS selectors rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, rsibling = /[+~]/, rescape = /'|\\/g, // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), funescape = function( _, escaped, escapedWhitespace ) { var high = "0x" + escaped - 0x10000; // NaN means non-codepoint // Support: Firefox<24 // Workaround erroneous numeric interpretation of +"0x" return high !== high || escapedWhitespace ? escaped : high < 0 ? // BMP codepoint String.fromCharCode( high + 0x10000 ) : // Supplemental Plane codepoint (surrogate pair) String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); }, // Used for iframes // See setDocument() // Removing the function wrapper causes a "Permission Denied" // error in IE unloadHandler = function() { setDocument(); }; // Optimize for push.apply( _, NodeList ) try { push.apply( (arr = slice.call( preferredDoc.childNodes )), preferredDoc.childNodes ); // Support: Android<4.0 // Detect silently failing push.apply arr[ preferredDoc.childNodes.length ].nodeType; } catch ( e ) { push = { apply: arr.length ? // Leverage slice if possible function( target, els ) { push_native.apply( target, slice.call(els) ); } : // Support: IE<9 // Otherwise append directly function( target, els ) { var j = target.length, i = 0; // Can't trust NodeList.length while ( (target[j++] = els[i++]) ) {} target.length = j - 1; } }; } function Sizzle( selector, context, results, seed ) { var m, i, elem, nid, nidselect, match, groups, newSelector, newContext = context && context.ownerDocument, // nodeType defaults to 9, since context defaults to document nodeType = context ? context.nodeType : 9; results = results || []; // Return early from calls with invalid selector or context if ( typeof selector !== "string" || !selector || nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { return results; } // Try to shortcut find operations (as opposed to filters) in HTML documents if ( !seed ) { if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { setDocument( context ); } context = context || document; if ( documentIsHTML ) { // If the selector is sufficiently simple, try using a "get*By*" DOM method // (excepting DocumentFragment context, where the methods don't exist) if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { // ID selector if ( (m = match[1]) ) { // Document context if ( nodeType === 9 ) { if ( (elem = context.getElementById( m )) ) { // Support: IE, Opera, Webkit // TODO: identify versions // getElementById can match elements by name instead of ID if ( elem.id === m ) { results.push( elem ); return results; } } else { return results; } // Element context } else { // Support: IE, Opera, Webkit // TODO: identify versions // getElementById can match elements by name instead of ID if ( newContext && (elem = newContext.getElementById( m )) && contains( context, elem ) && elem.id === m ) { results.push( elem ); return results; } } // Type selector } else if ( match[2] ) { push.apply( results, context.getElementsByTagName( selector ) ); return results; // Class selector } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { push.apply( results, context.getElementsByClassName( m ) ); return results; } } // Take advantage of querySelectorAll if ( support.qsa && !compilerCache[ selector + " " ] && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { if ( nodeType !== 1 ) { newContext = context; newSelector = selector; // qSA looks outside Element context, which is not what we want // Thanks to Andrew Dupont for this workaround technique // Support: IE <=8 // Exclude object elements } else if ( context.nodeName.toLowerCase() !== "object" ) { // Capture the context ID, setting it first if necessary if ( (nid = context.getAttribute( "id" )) ) { nid = nid.replace( rescape, "\\$&" ); } else { context.setAttribute( "id", (nid = expando) ); } // Prefix every selector in the list groups = tokenize( selector ); i = groups.length; nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']"; while ( i-- ) { groups[i] = nidselect + " " + toSelector( groups[i] ); } newSelector = groups.join( "," ); // Expand context for sibling selectors newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; } if ( newSelector ) { try { push.apply( results, newContext.querySelectorAll( newSelector ) ); return results; } catch ( qsaError ) { } finally { if ( nid === expando ) { context.removeAttribute( "id" ); } } } } } } // All others return select( selector.replace( rtrim, "$1" ), context, results, seed ); } /** * Create key-value caches of limited size * @returns {function(string, object)} Returns the Object data after storing it on itself with * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) * deleting the oldest entry */ function createCache() { var keys = []; function cache( key, value ) { // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) if ( keys.push( key + " " ) > Expr.cacheLength ) { // Only keep the most recent entries delete cache[ keys.shift() ]; } return (cache[ key + " " ] = value); } return cache; } /** * Mark a function for special use by Sizzle * @param {Function} fn The function to mark */ function markFunction( fn ) { fn[ expando ] = true; return fn; } /** * Support testing using an element * @param {Function} fn Passed the created div and expects a boolean result */ function assert( fn ) { var div = document.createElement("div"); try { return !!fn( div ); } catch (e) { return false; } finally { // Remove from its parent by default if ( div.parentNode ) { div.parentNode.removeChild( div ); } // release memory in IE div = null; } } /** * Adds the same handler for all of the specified attrs * @param {String} attrs Pipe-separated list of attributes * @param {Function} handler The method that will be applied */ function addHandle( attrs, handler ) { var arr = attrs.split("|"), i = arr.length; while ( i-- ) { Expr.attrHandle[ arr[i] ] = handler; } } /** * Checks document order of two siblings * @param {Element} a * @param {Element} b * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b */ function siblingCheck( a, b ) { var cur = b && a, diff = cur && a.nodeType === 1 && b.nodeType === 1 && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE ); // Use IE sourceIndex if available on both nodes if ( diff ) { return diff; } // Check if b follows a if ( cur ) { while ( (cur = cur.nextSibling) ) { if ( cur === b ) { return -1; } } } return a ? 1 : -1; } /** * Returns a function to use in pseudos for input types * @param {String} type */ function createInputPseudo( type ) { return function( elem ) { var name = elem.nodeName.toLowerCase(); return name === "input" && elem.type === type; }; } /** * Returns a function to use in pseudos for buttons * @param {String} type */ function createButtonPseudo( type ) { return function( elem ) { var name = elem.nodeName.toLowerCase(); return (name === "input" || name === "button") && elem.type === type; }; } /** * Returns a function to use in pseudos for positionals * @param {Function} fn */ function createPositionalPseudo( fn ) { return markFunction(function( argument ) { argument = +argument; return markFunction(function( seed, matches ) { var j, matchIndexes = fn( [], seed.length, argument ), i = matchIndexes.length; // Match elements found at the specified indexes while ( i-- ) { if ( seed[ (j = matchIndexes[i]) ] ) { seed[j] = !(matches[j] = seed[j]); } } }); }); } /** * Checks a node for validity as a Sizzle context * @param {Element|Object=} context * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value */ function testContext( context ) { return context && typeof context.getElementsByTagName !== "undefined" && context; } // Expose support vars for convenience support = Sizzle.support = {}; /** * Detects XML nodes * @param {Element|Object} elem An element or a document * @returns {Boolean} True iff elem is a non-HTML XML node */ isXML = Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist // (such as loading iframes in IE - #4833) var documentElement = elem && (elem.ownerDocument || elem).documentElement; return documentElement ? documentElement.nodeName !== "HTML" : false; }; /** * Sets document-related variables once based on the current document * @param {Element|Object} [doc] An element or document object to use to set the document * @returns {Object} Returns the current document */ setDocument = Sizzle.setDocument = function( node ) { var hasCompare, parent, doc = node ? node.ownerDocument || node : preferredDoc; // Return early if doc is invalid or already selected if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { return document; } // Update global variables document = doc; docElem = document.documentElement; documentIsHTML = !isXML( document ); // Support: IE 9-11, Edge // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) if ( (parent = document.defaultView) && parent.top !== parent ) { // Support: IE 11 if ( parent.addEventListener ) { parent.addEventListener( "unload", unloadHandler, false ); // Support: IE 9 - 10 only } else if ( parent.attachEvent ) { parent.attachEvent( "onunload", unloadHandler ); } } /* Attributes ---------------------------------------------------------------------- */ // Support: IE<8 // Verify that getAttribute really returns attributes and not properties // (excepting IE8 booleans) support.attributes = assert(function( div ) { div.className = "i"; return !div.getAttribute("className"); }); /* getElement(s)By* ---------------------------------------------------------------------- */ // Check if getElementsByTagName("*") returns only elements support.getElementsByTagName = assert(function( div ) { div.appendChild( document.createComment("") ); return !div.getElementsByTagName("*").length; }); // Support: IE<9 support.getElementsByClassName = rnative.test( document.getElementsByClassName ); // Support: IE<10 // Check if getElementById returns elements by name // The broken getElementById methods don't pick up programatically-set names, // so use a roundabout getElementsByName test support.getById = assert(function( div ) { docElem.appendChild( div ).id = expando; return !document.getElementsByName || !document.getElementsByName( expando ).length; }); // ID find and filter if ( support.getById ) { Expr.find["ID"] = function( id, context ) { if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { var m = context.getElementById( id ); return m ? [ m ] : []; } }; Expr.filter["ID"] = function( id ) { var attrId = id.replace( runescape, funescape ); return function( elem ) { return elem.getAttribute("id") === attrId; }; }; } else { // Support: IE6/7 // getElementById is not reliable as a find shortcut delete Expr.find["ID"]; Expr.filter["ID"] = function( id ) { var attrId = id.replace( runescape, funescape ); return function( elem ) { var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); return node && node.value === attrId; }; }; } // Tag Expr.find["TAG"] = support.getElementsByTagName ? function( tag, context ) { if ( typeof context.getElementsByTagName !== "undefined" ) { return context.getElementsByTagName( tag ); // DocumentFragment nodes don't have gEBTN } else if ( support.qsa ) { return context.querySelectorAll( tag ); } } : function( tag, context ) { var elem, tmp = [], i = 0, // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too results = context.getElementsByTagName( tag ); // Filter out possible comments if ( tag === "*" ) { while ( (elem = results[i++]) ) { if ( elem.nodeType === 1 ) { tmp.push( elem ); } } return tmp; } return results; }; // Class Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { return context.getElementsByClassName( className ); } }; /* QSA/matchesSelector ---------------------------------------------------------------------- */ // QSA and matchesSelector support // matchesSelector(:active) reports false when true (IE9/Opera 11.5) rbuggyMatches = []; // qSa(:focus) reports false when true (Chrome 21) // We allow this because of a bug in IE8/9 that throws an error // whenever `document.activeElement` is accessed on an iframe // So, we allow :focus to pass through QSA all the time to avoid the IE error // See http://bugs.jquery.com/ticket/13378 rbuggyQSA = []; if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { // Build QSA regex // Regex strategy adopted from Diego Perini assert(function( div ) { // Select is set to empty string on purpose // This is to test IE's treatment of not explicitly // setting a boolean content attribute, // since its presence should be enough // http://bugs.jquery.com/ticket/12359 docElem.appendChild( div ).innerHTML = "" + ""; // Support: IE8, Opera 11-12.16 // Nothing should be selected when empty strings follow ^= or $= or *= // The test attribute must be unknown in Opera but "safe" for WinRT // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section if ( div.querySelectorAll("[msallowcapture^='']").length ) { rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); } // Support: IE8 // Boolean attributes and "value" are not treated correctly if ( !div.querySelectorAll("[selected]").length ) { rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); } // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { rbuggyQSA.push("~="); } // Webkit/Opera - :checked should return selected option elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked // IE8 throws error here and will not see later tests if ( !div.querySelectorAll(":checked").length ) { rbuggyQSA.push(":checked"); } // Support: Safari 8+, iOS 8+ // https://bugs.webkit.org/show_bug.cgi?id=136851 // In-page `selector#id sibing-combinator selector` fails if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { rbuggyQSA.push(".#.+[+~]"); } }); assert(function( div ) { // Support: Windows 8 Native Apps // The type and name attributes are restricted during .innerHTML assignment var input = document.createElement("input"); input.setAttribute( "type", "hidden" ); div.appendChild( input ).setAttribute( "name", "D" ); // Support: IE8 // Enforce case-sensitivity of name attribute if ( div.querySelectorAll("[name=d]").length ) { rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); } // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) // IE8 throws error here and will not see later tests if ( !div.querySelectorAll(":enabled").length ) { rbuggyQSA.push( ":enabled", ":disabled" ); } // Opera 10-11 does not throw on post-comma invalid pseudos div.querySelectorAll("*,:x"); rbuggyQSA.push(",.*:"); }); } if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector) )) ) { assert(function( div ) { // Check to see if it's possible to do matchesSelector // on a disconnected node (IE 9) support.disconnectedMatch = matches.call( div, "div" ); // This should fail with an exception // Gecko does not error, returns false instead matches.call( div, "[s!='']:x" ); rbuggyMatches.push( "!=", pseudos ); }); } rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); /* Contains ---------------------------------------------------------------------- */ hasCompare = rnative.test( docElem.compareDocumentPosition ); // Element contains another // Purposefully self-exclusive // As in, an element does not contain itself contains = hasCompare || rnative.test( docElem.contains ) ? function( a, b ) { var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; return a === bup || !!( bup && bup.nodeType === 1 && ( adown.contains ? adown.contains( bup ) : a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 )); } : function( a, b ) { if ( b ) { while ( (b = b.parentNode) ) { if ( b === a ) { return true; } } } return false; }; /* Sorting ---------------------------------------------------------------------- */ // Document order sorting sortOrder = hasCompare ? function( a, b ) { // Flag for duplicate removal if ( a === b ) { hasDuplicate = true; return 0; } // Sort on method existence if only one input has compareDocumentPosition var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; if ( compare ) { return compare; } // Calculate position if both inputs belong to the same document compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? a.compareDocumentPosition( b ) : // Otherwise we know they are disconnected 1; // Disconnected nodes if ( compare & 1 || (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { // Choose the first element that is related to our preferred document if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { return -1; } if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { return 1; } // Maintain original order return sortInput ? ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : 0; } return compare & 4 ? -1 : 1; } : function( a, b ) { // Exit early if the nodes are identical if ( a === b ) { hasDuplicate = true; return 0; } var cur, i = 0, aup = a.parentNode, bup = b.parentNode, ap = [ a ], bp = [ b ]; // Parentless nodes are either documents or disconnected if ( !aup || !bup ) { return a === document ? -1 : b === document ? 1 : aup ? -1 : bup ? 1 : sortInput ? ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : 0; // If the nodes are siblings, we can do a quick check } else if ( aup === bup ) { return siblingCheck( a, b ); } // Otherwise we need full lists of their ancestors for comparison cur = a; while ( (cur = cur.parentNode) ) { ap.unshift( cur ); } cur = b; while ( (cur = cur.parentNode) ) { bp.unshift( cur ); } // Walk down the tree looking for a discrepancy while ( ap[i] === bp[i] ) { i++; } return i ? // Do a sibling check if the nodes have a common ancestor siblingCheck( ap[i], bp[i] ) : // Otherwise nodes in our document sort first ap[i] === preferredDoc ? -1 : bp[i] === preferredDoc ? 1 : 0; }; return document; }; Sizzle.matches = function( expr, elements ) { return Sizzle( expr, null, null, elements ); }; Sizzle.matchesSelector = function( elem, expr ) { // Set document vars if needed if ( ( elem.ownerDocument || elem ) !== document ) { setDocument( elem ); } // Make sure that attribute selectors are quoted expr = expr.replace( rattributeQuotes, "='$1']" ); if ( support.matchesSelector && documentIsHTML && !compilerCache[ expr + " " ] && ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { try { var ret = matches.call( elem, expr ); // IE 9's matchesSelector returns false on disconnected nodes if ( ret || support.disconnectedMatch || // As well, disconnected nodes are said to be in a document // fragment in IE 9 elem.document && elem.document.nodeType !== 11 ) { return ret; } } catch (e) {} } return Sizzle( expr, document, null, [ elem ] ).length > 0; }; Sizzle.contains = function( context, elem ) { // Set document vars if needed if ( ( context.ownerDocument || context ) !== document ) { setDocument( context ); } return contains( context, elem ); }; Sizzle.attr = function( elem, name ) { // Set document vars if needed if ( ( elem.ownerDocument || elem ) !== document ) { setDocument( elem ); } var fn = Expr.attrHandle[ name.toLowerCase() ], // Don't get fooled by Object.prototype properties (jQuery #13807) val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? fn( elem, name, !documentIsHTML ) : undefined; return val !== undefined ? val : support.attributes || !documentIsHTML ? elem.getAttribute( name ) : (val = elem.getAttributeNode(name)) && val.specified ? val.value : null; }; Sizzle.error = function( msg ) { throw new Error( "Syntax error, unrecognized expression: " + msg ); }; /** * Document sorting and removing duplicates * @param {ArrayLike} results */ Sizzle.uniqueSort = function( results ) { var elem, duplicates = [], j = 0, i = 0; // Unless we *know* we can detect duplicates, assume their presence hasDuplicate = !support.detectDuplicates; sortInput = !support.sortStable && results.slice( 0 ); results.sort( sortOrder ); if ( hasDuplicate ) { while ( (elem = results[i++]) ) { if ( elem === results[ i ] ) { j = duplicates.push( i ); } } while ( j-- ) { results.splice( duplicates[ j ], 1 ); } } // Clear input after sorting to release objects // See https://github.com/jquery/sizzle/pull/225 sortInput = null; return results; }; /** * Utility function for retrieving the text value of an array of DOM nodes * @param {Array|Element} elem */ getText = Sizzle.getText = function( elem ) { var node, ret = "", i = 0, nodeType = elem.nodeType; if ( !nodeType ) { // If no nodeType, this is expected to be an array while ( (node = elem[i++]) ) { // Do not traverse comment nodes ret += getText( node ); } } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { // Use textContent for elements // innerText usage removed for consistency of new lines (jQuery #11153) if ( typeof elem.textContent === "string" ) { return elem.textContent; } else { // Traverse its children for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { ret += getText( elem ); } } } else if ( nodeType === 3 || nodeType === 4 ) { return elem.nodeValue; } // Do not include comment or processing instruction nodes return ret; }; Expr = Sizzle.selectors = { // Can be adjusted by the user cacheLength: 50, createPseudo: markFunction, match: matchExpr, attrHandle: {}, find: {}, relative: { ">": { dir: "parentNode", first: true }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: true }, "~": { dir: "previousSibling" } }, preFilter: { "ATTR": function( match ) { match[1] = match[1].replace( runescape, funescape ); // Move the given value to match[3] whether quoted or unquoted match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); if ( match[2] === "~=" ) { match[3] = " " + match[3] + " "; } return match.slice( 0, 4 ); }, "CHILD": function( match ) { /* matches from matchExpr["CHILD"] 1 type (only|nth|...) 2 what (child|of-type) 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) 4 xn-component of xn+y argument ([+-]?\d*n|) 5 sign of xn-component 6 x of xn-component 7 sign of y-component 8 y of y-component */ match[1] = match[1].toLowerCase(); if ( match[1].slice( 0, 3 ) === "nth" ) { // nth-* requires argument if ( !match[3] ) { Sizzle.error( match[0] ); } // numeric x and y parameters for Expr.filter.CHILD // remember that false/true cast respectively to 0/1 match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); // other types prohibit arguments } else if ( match[3] ) { Sizzle.error( match[0] ); } return match; }, "PSEUDO": function( match ) { var excess, unquoted = !match[6] && match[2]; if ( matchExpr["CHILD"].test( match[0] ) ) { return null; } // Accept quoted arguments as-is if ( match[3] ) { match[2] = match[4] || match[5] || ""; // Strip excess characters from unquoted arguments } else if ( unquoted && rpseudo.test( unquoted ) && // Get excess from tokenize (recursively) (excess = tokenize( unquoted, true )) && // advance to the next closing parenthesis (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { // excess is a negative index match[0] = match[0].slice( 0, excess ); match[2] = unquoted.slice( 0, excess ); } // Return only captures needed by the pseudo filter method (type and argument) return match.slice( 0, 3 ); } }, filter: { "TAG": function( nodeNameSelector ) { var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); return nodeNameSelector === "*" ? function() { return true; } : function( elem ) { return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; }; }, "CLASS": function( className ) { var pattern = classCache[ className + " " ]; return pattern || (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && classCache( className, function( elem ) { return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); }); }, "ATTR": function( name, operator, check ) { return function( elem ) { var result = Sizzle.attr( elem, name ); if ( result == null ) { return operator === "!="; } if ( !operator ) { return true; } result += ""; return operator === "=" ? result === check : operator === "!=" ? result !== check : operator === "^=" ? check && result.indexOf( check ) === 0 : operator === "*=" ? check && result.indexOf( check ) > -1 : operator === "$=" ? check && result.slice( -check.length ) === check : operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : false; }; }, "CHILD": function( type, what, argument, first, last ) { var simple = type.slice( 0, 3 ) !== "nth", forward = type.slice( -4 ) !== "last", ofType = what === "of-type"; return first === 1 && last === 0 ? // Shortcut for :nth-*(n) function( elem ) { return !!elem.parentNode; } : function( elem, context, xml ) { var cache, uniqueCache, outerCache, node, nodeIndex, start, dir = simple !== forward ? "nextSibling" : "previousSibling", parent = elem.parentNode, name = ofType && elem.nodeName.toLowerCase(), useCache = !xml && !ofType, diff = false; if ( parent ) { // :(first|last|only)-(child|of-type) if ( simple ) { while ( dir ) { node = elem; while ( (node = node[ dir ]) ) { if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { return false; } } // Reverse direction for :only-* (if we haven't yet done so) start = dir = type === "only" && !start && "nextSibling"; } return true; } start = [ forward ? parent.firstChild : parent.lastChild ]; // non-xml :nth-child(...) stores cache data on `parent` if ( forward && useCache ) { // Seek `elem` from a previously-cached index // ...in a gzip-friendly way node = parent; outerCache = node[ expando ] || (node[ expando ] = {}); // Support: IE <9 only // Defend against cloned attroperties (jQuery gh-1709) uniqueCache = outerCache[ node.uniqueID ] || (outerCache[ node.uniqueID ] = {}); cache = uniqueCache[ type ] || []; nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; diff = nodeIndex && cache[ 2 ]; node = nodeIndex && parent.childNodes[ nodeIndex ]; while ( (node = ++nodeIndex && node && node[ dir ] || // Fallback to seeking `elem` from the start (diff = nodeIndex = 0) || start.pop()) ) { // When found, cache indexes on `parent` and break if ( node.nodeType === 1 && ++diff && node === elem ) { uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; break; } } } else { // Use previously-cached element index if available if ( useCache ) { // ...in a gzip-friendly way node = elem; outerCache = node[ expando ] || (node[ expando ] = {}); // Support: IE <9 only // Defend against cloned attroperties (jQuery gh-1709) uniqueCache = outerCache[ node.uniqueID ] || (outerCache[ node.uniqueID ] = {}); cache = uniqueCache[ type ] || []; nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; diff = nodeIndex; } // xml :nth-child(...) // or :nth-last-child(...) or :nth(-last)?-of-type(...) if ( diff === false ) { // Use the same loop as above to seek `elem` from the start while ( (node = ++nodeIndex && node && node[ dir ] || (diff = nodeIndex = 0) || start.pop()) ) { if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { // Cache the index of each encountered element if ( useCache ) { outerCache = node[ expando ] || (node[ expando ] = {}); // Support: IE <9 only // Defend against cloned attroperties (jQuery gh-1709) uniqueCache = outerCache[ node.uniqueID ] || (outerCache[ node.uniqueID ] = {}); uniqueCache[ type ] = [ dirruns, diff ]; } if ( node === elem ) { break; } } } } } // Incorporate the offset, then check against cycle size diff -= last; return diff === first || ( diff % first === 0 && diff / first >= 0 ); } }; }, "PSEUDO": function( pseudo, argument ) { // pseudo-class names are case-insensitive // http://www.w3.org/TR/selectors/#pseudo-classes // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters // Remember that setFilters inherits from pseudos var args, fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || Sizzle.error( "unsupported pseudo: " + pseudo ); // The user may use createPseudo to indicate that // arguments are needed to create the filter function // just as Sizzle does if ( fn[ expando ] ) { return fn( argument ); } // But maintain support for old signatures if ( fn.length > 1 ) { args = [ pseudo, pseudo, "", argument ]; return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? markFunction(function( seed, matches ) { var idx, matched = fn( seed, argument ), i = matched.length; while ( i-- ) { idx = indexOf( seed, matched[i] ); seed[ idx ] = !( matches[ idx ] = matched[i] ); } }) : function( elem ) { return fn( elem, 0, args ); }; } return fn; } }, pseudos: { // Potentially complex pseudos "not": markFunction(function( selector ) { // Trim the selector passed to compile // to avoid treating leading and trailing // spaces as combinators var input = [], results = [], matcher = compile( selector.replace( rtrim, "$1" ) ); return matcher[ expando ] ? markFunction(function( seed, matches, context, xml ) { var elem, unmatched = matcher( seed, null, xml, [] ), i = seed.length; // Match elements unmatched by `matcher` while ( i-- ) { if ( (elem = unmatched[i]) ) { seed[i] = !(matches[i] = elem); } } }) : function( elem, context, xml ) { input[0] = elem; matcher( input, null, xml, results ); // Don't keep the element (issue #299) input[0] = null; return !results.pop(); }; }), "has": markFunction(function( selector ) { return function( elem ) { return Sizzle( selector, elem ).length > 0; }; }), "contains": markFunction(function( text ) { text = text.replace( runescape, funescape ); return function( elem ) { return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; }; }), // "Whether an element is represented by a :lang() selector // is based solely on the element's language value // being equal to the identifier C, // or beginning with the identifier C immediately followed by "-". // The matching of C against the element's language value is performed case-insensitively. // The identifier C does not have to be a valid language name." // http://www.w3.org/TR/selectors/#lang-pseudo "lang": markFunction( function( lang ) { // lang value must be a valid identifier if ( !ridentifier.test(lang || "") ) { Sizzle.error( "unsupported lang: " + lang ); } lang = lang.replace( runescape, funescape ).toLowerCase(); return function( elem ) { var elemLang; do { if ( (elemLang = documentIsHTML ? elem.lang : elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { elemLang = elemLang.toLowerCase(); return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; } } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); return false; }; }), // Miscellaneous "target": function( elem ) { var hash = window.location && window.location.hash; return hash && hash.slice( 1 ) === elem.id; }, "root": function( elem ) { return elem === docElem; }, "focus": function( elem ) { return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); }, // Boolean properties "enabled": function( elem ) { return elem.disabled === false; }, "disabled": function( elem ) { return elem.disabled === true; }, "checked": function( elem ) { // In CSS3, :checked should return both checked and selected elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked var nodeName = elem.nodeName.toLowerCase(); return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); }, "selected": function( elem ) { // Accessing this property makes selected-by-default // options in Safari work properly if ( elem.parentNode ) { elem.parentNode.selectedIndex; } return elem.selected === true; }, // Contents "empty": function( elem ) { // http://www.w3.org/TR/selectors/#empty-pseudo // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), // but not by others (comment: 8; processing instruction: 7; etc.) // nodeType < 6 works because attributes (2) do not appear as children for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { if ( elem.nodeType < 6 ) { return false; } } return true; }, "parent": function( elem ) { return !Expr.pseudos["empty"]( elem ); }, // Element/input types "header": function( elem ) { return rheader.test( elem.nodeName ); }, "input": function( elem ) { return rinputs.test( elem.nodeName ); }, "button": function( elem ) { var name = elem.nodeName.toLowerCase(); return name === "input" && elem.type === "button" || name === "button"; }, "text": function( elem ) { var attr; return elem.nodeName.toLowerCase() === "input" && elem.type === "text" && // Support: IE<8 // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); }, // Position-in-collection "first": createPositionalPseudo(function() { return [ 0 ]; }), "last": createPositionalPseudo(function( matchIndexes, length ) { return [ length - 1 ]; }), "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { return [ argument < 0 ? argument + length : argument ]; }), "even": createPositionalPseudo(function( matchIndexes, length ) { var i = 0; for ( ; i < length; i += 2 ) { matchIndexes.push( i ); } return matchIndexes; }), "odd": createPositionalPseudo(function( matchIndexes, length ) { var i = 1; for ( ; i < length; i += 2 ) { matchIndexes.push( i ); } return matchIndexes; }), "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { var i = argument < 0 ? argument + length : argument; for ( ; --i >= 0; ) { matchIndexes.push( i ); } return matchIndexes; }), "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { var i = argument < 0 ? argument + length : argument; for ( ; ++i < length; ) { matchIndexes.push( i ); } return matchIndexes; }) } }; Expr.pseudos["nth"] = Expr.pseudos["eq"]; // Add button/input type pseudos for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { Expr.pseudos[ i ] = createInputPseudo( i ); } for ( i in { submit: true, reset: true } ) { Expr.pseudos[ i ] = createButtonPseudo( i ); } // Easy API for creating new setFilters function setFilters() {} setFilters.prototype = Expr.filters = Expr.pseudos; Expr.setFilters = new setFilters(); tokenize = Sizzle.tokenize = function( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ selector + " " ]; if ( cached ) { return parseOnly ? 0 : cached.slice( 0 ); } soFar = selector; groups = []; preFilters = Expr.preFilter; while ( soFar ) { // Comma and first run if ( !matched || (match = rcomma.exec( soFar )) ) { if ( match ) { // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } groups.push( (tokens = []) ); } matched = false; // Combinators if ( (match = rcombinators.exec( soFar )) ) { matched = match.shift(); tokens.push({ value: matched, // Cast descendant combinators to space type: match[0].replace( rtrim, " " ) }); soFar = soFar.slice( matched.length ); } // Filters for ( type in Expr.filter ) { if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) { matched = match.shift(); tokens.push({ value: matched, type: type, matches: match }); soFar = soFar.slice( matched.length ); } } if ( !matched ) { break; } } // Return the length of the invalid excess // if we're just parsing // Otherwise, throw an error or return tokens return parseOnly ? soFar.length : soFar ? Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); }; function toSelector( tokens ) { var i = 0, len = tokens.length, selector = ""; for ( ; i < len; i++ ) { selector += tokens[i].value; } return selector; } function addCombinator( matcher, combinator, base ) { var dir = combinator.dir, checkNonElements = base && dir === "parentNode", doneName = done++; return combinator.first ? // Check against closest ancestor/preceding element function( elem, context, xml ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { return matcher( elem, context, xml ); } } } : // Check against all ancestor/preceding elements function( elem, context, xml ) { var oldCache, uniqueCache, outerCache, newCache = [ dirruns, doneName ]; // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching if ( xml ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { if ( matcher( elem, context, xml ) ) { return true; } } } } else { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { outerCache = elem[ expando ] || (elem[ expando ] = {}); // Support: IE <9 only // Defend against cloned attroperties (jQuery gh-1709) uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); if ( (oldCache = uniqueCache[ dir ]) && oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { // Assign to newCache so results back-propagate to previous elements return (newCache[ 2 ] = oldCache[ 2 ]); } else { // Reuse newcache so results back-propagate to previous elements uniqueCache[ dir ] = newCache; // A match means we're done; a fail means we have to keep checking if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { return true; } } } } } }; } function elementMatcher( matchers ) { return matchers.length > 1 ? function( elem, context, xml ) { var i = matchers.length; while ( i-- ) { if ( !matchers[i]( elem, context, xml ) ) { return false; } } return true; } : matchers[0]; } function multipleContexts( selector, contexts, results ) { var i = 0, len = contexts.length; for ( ; i < len; i++ ) { Sizzle( selector, contexts[i], results ); } return results; } function condense( unmatched, map, filter, context, xml ) { var elem, newUnmatched = [], i = 0, len = unmatched.length, mapped = map != null; for ( ; i < len; i++ ) { if ( (elem = unmatched[i]) ) { if ( !filter || filter( elem, context, xml ) ) { newUnmatched.push( elem ); if ( mapped ) { map.push( i ); } } } } return newUnmatched; } function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { if ( postFilter && !postFilter[ expando ] ) { postFilter = setMatcher( postFilter ); } if ( postFinder && !postFinder[ expando ] ) { postFinder = setMatcher( postFinder, postSelector ); } return markFunction(function( seed, results, context, xml ) { var temp, i, elem, preMap = [], postMap = [], preexisting = results.length, // Get initial elements from seed or context elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), // Prefilter to get matcher input, preserving a map for seed-results synchronization matcherIn = preFilter && ( seed || !selector ) ? condense( elems, preMap, preFilter, context, xml ) : elems, matcherOut = matcher ? // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, postFinder || ( seed ? preFilter : preexisting || postFilter ) ? // ...intermediate processing is necessary [] : // ...otherwise use results directly results : matcherIn; // Find primary matches if ( matcher ) { matcher( matcherIn, matcherOut, context, xml ); } // Apply postFilter if ( postFilter ) { temp = condense( matcherOut, postMap ); postFilter( temp, [], context, xml ); // Un-match failing elements by moving them back to matcherIn i = temp.length; while ( i-- ) { if ( (elem = temp[i]) ) { matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); } } } if ( seed ) { if ( postFinder || preFilter ) { if ( postFinder ) { // Get the final matcherOut by condensing this intermediate into postFinder contexts temp = []; i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) ) { // Restore matcherIn since elem is not yet a final match temp.push( (matcherIn[i] = elem) ); } } postFinder( null, (matcherOut = []), temp, xml ); } // Move matched elements from seed to results to keep them synchronized i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) && (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { seed[temp] = !(results[temp] = elem); } } } // Add elements to results, through postFinder if defined } else { matcherOut = condense( matcherOut === results ? matcherOut.splice( preexisting, matcherOut.length ) : matcherOut ); if ( postFinder ) { postFinder( null, results, matcherOut, xml ); } else { push.apply( results, matcherOut ); } } }); } function matcherFromTokens( tokens ) { var checkContext, matcher, j, len = tokens.length, leadingRelative = Expr.relative[ tokens[0].type ], implicitRelative = leadingRelative || Expr.relative[" "], i = leadingRelative ? 1 : 0, // The foundational matcher ensures that elements are reachable from top-level context(s) matchContext = addCombinator( function( elem ) { return elem === checkContext; }, implicitRelative, true ), matchAnyContext = addCombinator( function( elem ) { return indexOf( checkContext, elem ) > -1; }, implicitRelative, true ), matchers = [ function( elem, context, xml ) { var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( (checkContext = context).nodeType ? matchContext( elem, context, xml ) : matchAnyContext( elem, context, xml ) ); // Avoid hanging onto element (issue #299) checkContext = null; return ret; } ]; for ( ; i < len; i++ ) { if ( (matcher = Expr.relative[ tokens[i].type ]) ) { matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; } else { matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); // Return special upon seeing a positional matcher if ( matcher[ expando ] ) { // Find the next relative operator (if any) for proper handling j = ++i; for ( ; j < len; j++ ) { if ( Expr.relative[ tokens[j].type ] ) { break; } } return setMatcher( i > 1 && elementMatcher( matchers ), i > 1 && toSelector( // If the preceding token was a descendant combinator, insert an implicit any-element `*` tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) ).replace( rtrim, "$1" ), matcher, i < j && matcherFromTokens( tokens.slice( i, j ) ), j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), j < len && toSelector( tokens ) ); } matchers.push( matcher ); } } return elementMatcher( matchers ); } function matcherFromGroupMatchers( elementMatchers, setMatchers ) { var bySet = setMatchers.length > 0, byElement = elementMatchers.length > 0, superMatcher = function( seed, context, xml, results, outermost ) { var elem, j, matcher, matchedCount = 0, i = "0", unmatched = seed && [], setMatched = [], contextBackup = outermostContext, // We must always have either seed elements or outermost context elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), // Use integer dirruns iff this is the outermost matcher dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), len = elems.length; if ( outermost ) { outermostContext = context === document || context || outermost; } // Add elements passing elementMatchers directly to results // Support: IE<9, Safari // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id for ( ; i !== len && (elem = elems[i]) != null; i++ ) { if ( byElement && elem ) { j = 0; if ( !context && elem.ownerDocument !== document ) { setDocument( elem ); xml = !documentIsHTML; } while ( (matcher = elementMatchers[j++]) ) { if ( matcher( elem, context || document, xml) ) { results.push( elem ); break; } } if ( outermost ) { dirruns = dirrunsUnique; } } // Track unmatched elements for set filters if ( bySet ) { // They will have gone through all possible matchers if ( (elem = !matcher && elem) ) { matchedCount--; } // Lengthen the array for every element, matched or not if ( seed ) { unmatched.push( elem ); } } } // `i` is now the count of elements visited above, and adding it to `matchedCount` // makes the latter nonnegative. matchedCount += i; // Apply set filters to unmatched elements // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` // equals `i`), unless we didn't visit _any_ elements in the above loop because we have // no element matchers and no seed. // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that // case, which will result in a "00" `matchedCount` that differs from `i` but is also // numerically zero. if ( bySet && i !== matchedCount ) { j = 0; while ( (matcher = setMatchers[j++]) ) { matcher( unmatched, setMatched, context, xml ); } if ( seed ) { // Reintegrate element matches to eliminate the need for sorting if ( matchedCount > 0 ) { while ( i-- ) { if ( !(unmatched[i] || setMatched[i]) ) { setMatched[i] = pop.call( results ); } } } // Discard index placeholder values to get only actual matches setMatched = condense( setMatched ); } // Add matches to results push.apply( results, setMatched ); // Seedless set matches succeeding multiple successful matchers stipulate sorting if ( outermost && !seed && setMatched.length > 0 && ( matchedCount + setMatchers.length ) > 1 ) { Sizzle.uniqueSort( results ); } } // Override manipulation of globals by nested matchers if ( outermost ) { dirruns = dirrunsUnique; outermostContext = contextBackup; } return unmatched; }; return bySet ? markFunction( superMatcher ) : superMatcher; } compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { var i, setMatchers = [], elementMatchers = [], cached = compilerCache[ selector + " " ]; if ( !cached ) { // Generate a function of recursive functions that can be used to check each element if ( !match ) { match = tokenize( selector ); } i = match.length; while ( i-- ) { cached = matcherFromTokens( match[i] ); if ( cached[ expando ] ) { setMatchers.push( cached ); } else { elementMatchers.push( cached ); } } // Cache the compiled function cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); // Save selector and tokenization cached.selector = selector; } return cached; }; /** * A low-level selection function that works with Sizzle's compiled * selector functions * @param {String|Function} selector A selector or a pre-compiled * selector function built with Sizzle.compile * @param {Element} context * @param {Array} [results] * @param {Array} [seed] A set of elements to match against */ select = Sizzle.select = function( selector, context, results, seed ) { var i, tokens, token, type, find, compiled = typeof selector === "function" && selector, match = !seed && tokenize( (selector = compiled.selector || selector) ); results = results || []; // Try to minimize operations if there is only one selector in the list and no seed // (the latter of which guarantees us context) if ( match.length === 1 ) { // Reduce context if the leading compound selector is an ID tokens = match[0] = match[0].slice( 0 ); if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && support.getById && context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) { context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; if ( !context ) { return results; // Precompiled matchers will still verify ancestry, so step up a level } else if ( compiled ) { context = context.parentNode; } selector = selector.slice( tokens.shift().value.length ); } // Fetch a seed set for right-to-left matching i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; while ( i-- ) { token = tokens[i]; // Abort if we hit a combinator if ( Expr.relative[ (type = token.type) ] ) { break; } if ( (find = Expr.find[ type ]) ) { // Search, expanding context for leading sibling combinators if ( (seed = find( token.matches[0].replace( runescape, funescape ), rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context )) ) { // If seed is empty or no tokens remain, we can return early tokens.splice( i, 1 ); selector = seed.length && toSelector( tokens ); if ( !selector ) { push.apply( results, seed ); return results; } break; } } } } // Compile and execute a filtering function if one is not provided // Provide `match` to avoid retokenization if we modified the selector above ( compiled || compile( selector, match ) )( seed, context, !documentIsHTML, results, !context || rsibling.test( selector ) && testContext( context.parentNode ) || context ); return results; }; // One-time assignments // Sort stability support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; // Support: Chrome 14-35+ // Always assume duplicates if they aren't passed to the comparison function support.detectDuplicates = !!hasDuplicate; // Initialize against the default document setDocument(); // Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) // Detached nodes confoundingly follow *each other* support.sortDetached = assert(function( div1 ) { // Should return 1, but returns 4 (following) return div1.compareDocumentPosition( document.createElement("div") ) & 1; }); // Support: IE<8 // Prevent attribute/property "interpolation" // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx if ( !assert(function( div ) { div.innerHTML = ""; return div.firstChild.getAttribute("href") === "#" ; }) ) { addHandle( "type|href|height|width", function( elem, name, isXML ) { if ( !isXML ) { return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); } }); } // Support: IE<9 // Use defaultValue in place of getAttribute("value") if ( !support.attributes || !assert(function( div ) { div.innerHTML = ""; div.firstChild.setAttribute( "value", "" ); return div.firstChild.getAttribute( "value" ) === ""; }) ) { addHandle( "value", function( elem, name, isXML ) { if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { return elem.defaultValue; } }); } // Support: IE<9 // Use getAttributeNode to fetch booleans when getAttribute lies if ( !assert(function( div ) { return div.getAttribute("disabled") == null; }) ) { addHandle( booleans, function( elem, name, isXML ) { var val; if ( !isXML ) { return elem[ name ] === true ? name.toLowerCase() : (val = elem.getAttributeNode( name )) && val.specified ? val.value : null; } }); } return Sizzle; })( window ); jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[ ":" ] = jQuery.expr.pseudos; jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; jQuery.text = Sizzle.getText; jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains; var dir = function( elem, dir, until ) { var matched = [], truncate = until !== undefined; while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { if ( elem.nodeType === 1 ) { if ( truncate && jQuery( elem ).is( until ) ) { break; } matched.push( elem ); } } return matched; }; var siblings = function( n, elem ) { var matched = []; for ( ; n; n = n.nextSibling ) { if ( n.nodeType === 1 && n !== elem ) { matched.push( n ); } } return matched; }; var rneedsContext = jQuery.expr.match.needsContext; var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ ); var risSimple = /^.[^:#\[\.,]*$/; // Implement the identical functionality for filter and not function winnow( elements, qualifier, not ) { if ( jQuery.isFunction( qualifier ) ) { return jQuery.grep( elements, function( elem, i ) { /* jshint -W018 */ return !!qualifier.call( elem, i, elem ) !== not; } ); } if ( qualifier.nodeType ) { return jQuery.grep( elements, function( elem ) { return ( elem === qualifier ) !== not; } ); } if ( typeof qualifier === "string" ) { if ( risSimple.test( qualifier ) ) { return jQuery.filter( qualifier, elements, not ); } qualifier = jQuery.filter( qualifier, elements ); } return jQuery.grep( elements, function( elem ) { return ( indexOf.call( qualifier, elem ) > -1 ) !== not; } ); } jQuery.filter = function( expr, elems, not ) { var elem = elems[ 0 ]; if ( not ) { expr = ":not(" + expr + ")"; } return elems.length === 1 && elem.nodeType === 1 ? jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { return elem.nodeType === 1; } ) ); }; jQuery.fn.extend( { find: function( selector ) { var i, len = this.length, ret = [], self = this; if ( typeof selector !== "string" ) { return this.pushStack( jQuery( selector ).filter( function() { for ( i = 0; i < len; i++ ) { if ( jQuery.contains( self[ i ], this ) ) { return true; } } } ) ); } for ( i = 0; i < len; i++ ) { jQuery.find( selector, self[ i ], ret ); } // Needed because $( selector, context ) becomes $( context ).find( selector ) ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); ret.selector = this.selector ? this.selector + " " + selector : selector; return ret; }, filter: function( selector ) { return this.pushStack( winnow( this, selector || [], false ) ); }, not: function( selector ) { return this.pushStack( winnow( this, selector || [], true ) ); }, is: function( selector ) { return !!winnow( this, // If this is a positional/relative selector, check membership in the returned set // so $("p:first").is("p:last") won't return true for a doc with two "p". typeof selector === "string" && rneedsContext.test( selector ) ? jQuery( selector ) : selector || [], false ).length; } } ); // Initialize a jQuery object // A central reference to the root jQuery(document) var rootjQuery, // A simple way to check for HTML strings // Prioritize #id over to avoid XSS via location.hash (#9521) // Strict HTML recognition (#11290: must start with <) rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, init = jQuery.fn.init = function( selector, context, root ) { var match, elem; // HANDLE: $(""), $(null), $(undefined), $(false) if ( !selector ) { return this; } // Method init() accepts an alternate rootjQuery // so migrate can support jQuery.sub (gh-2101) root = root || rootjQuery; // Handle HTML strings if ( typeof selector === "string" ) { if ( selector[ 0 ] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { // Assume that strings that start and end with <> are HTML and skip the regex check match = [ null, selector, null ]; } else { match = rquickExpr.exec( selector ); } // Match html or make sure no context is specified for #id if ( match && ( match[ 1 ] || !context ) ) { // HANDLE: $(html) -> $(array) if ( match[ 1 ] ) { context = context instanceof jQuery ? context[ 0 ] : context; // Option to run scripts is true for back-compat // Intentionally let the error be thrown if parseHTML is not present jQuery.merge( this, jQuery.parseHTML( match[ 1 ], context && context.nodeType ? context.ownerDocument || context : document, true ) ); // HANDLE: $(html, props) if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { for ( match in context ) { // Properties of context are called as methods if possible if ( jQuery.isFunction( this[ match ] ) ) { this[ match ]( context[ match ] ); // ...and otherwise set as attributes } else { this.attr( match, context[ match ] ); } } } return this; // HANDLE: $(#id) } else { elem = document.getElementById( match[ 2 ] ); // Support: Blackberry 4.6 // gEBID returns nodes no longer in the document (#6963) if ( elem && elem.parentNode ) { // Inject the element directly into the jQuery object this.length = 1; this[ 0 ] = elem; } this.context = document; this.selector = selector; return this; } // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { return ( context || root ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { return this.constructor( context ).find( selector ); } // HANDLE: $(DOMElement) } else if ( selector.nodeType ) { this.context = this[ 0 ] = selector; this.length = 1; return this; // HANDLE: $(function) // Shortcut for document ready } else if ( jQuery.isFunction( selector ) ) { return root.ready !== undefined ? root.ready( selector ) : // Execute immediately if ready is not present selector( jQuery ); } if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } return jQuery.makeArray( selector, this ); }; // Give the init function the jQuery prototype for later instantiation init.prototype = jQuery.fn; // Initialize central reference rootjQuery = jQuery( document ); var rparentsprev = /^(?:parents|prev(?:Until|All))/, // Methods guaranteed to produce a unique set when starting from a unique set guaranteedUnique = { children: true, contents: true, next: true, prev: true }; jQuery.fn.extend( { has: function( target ) { var targets = jQuery( target, this ), l = targets.length; return this.filter( function() { var i = 0; for ( ; i < l; i++ ) { if ( jQuery.contains( this, targets[ i ] ) ) { return true; } } } ); }, closest: function( selectors, context ) { var cur, i = 0, l = this.length, matched = [], pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? jQuery( selectors, context || this.context ) : 0; for ( ; i < l; i++ ) { for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { // Always skip document fragments if ( cur.nodeType < 11 && ( pos ? pos.index( cur ) > -1 : // Don't pass non-elements to Sizzle cur.nodeType === 1 && jQuery.find.matchesSelector( cur, selectors ) ) ) { matched.push( cur ); break; } } } return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); }, // Determine the position of an element within the set index: function( elem ) { // No argument, return index in parent if ( !elem ) { return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; } // Index in selector if ( typeof elem === "string" ) { return indexOf.call( jQuery( elem ), this[ 0 ] ); } // Locate the position of the desired element return indexOf.call( this, // If it receives a jQuery object, the first element is used elem.jquery ? elem[ 0 ] : elem ); }, add: function( selector, context ) { return this.pushStack( jQuery.uniqueSort( jQuery.merge( this.get(), jQuery( selector, context ) ) ) ); }, addBack: function( selector ) { return this.add( selector == null ? this.prevObject : this.prevObject.filter( selector ) ); } } ); function sibling( cur, dir ) { while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} return cur; } jQuery.each( { parent: function( elem ) { var parent = elem.parentNode; return parent && parent.nodeType !== 11 ? parent : null; }, parents: function( elem ) { return dir( elem, "parentNode" ); }, parentsUntil: function( elem, i, until ) { return dir( elem, "parentNode", until ); }, next: function( elem ) { return sibling( elem, "nextSibling" ); }, prev: function( elem ) { return sibling( elem, "previousSibling" ); }, nextAll: function( elem ) { return dir( elem, "nextSibling" ); }, prevAll: function( elem ) { return dir( elem, "previousSibling" ); }, nextUntil: function( elem, i, until ) { return dir( elem, "nextSibling", until ); }, prevUntil: function( elem, i, until ) { return dir( elem, "previousSibling", until ); }, siblings: function( elem ) { return siblings( ( elem.parentNode || {} ).firstChild, elem ); }, children: function( elem ) { return siblings( elem.firstChild ); }, contents: function( elem ) { return elem.contentDocument || jQuery.merge( [], elem.childNodes ); } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { var matched = jQuery.map( this, fn, until ); if ( name.slice( -5 ) !== "Until" ) { selector = until; } if ( selector && typeof selector === "string" ) { matched = jQuery.filter( selector, matched ); } if ( this.length > 1 ) { // Remove duplicates if ( !guaranteedUnique[ name ] ) { jQuery.uniqueSort( matched ); } // Reverse order for parents* and prev-derivatives if ( rparentsprev.test( name ) ) { matched.reverse(); } } return this.pushStack( matched ); }; } ); var rnotwhite = ( /\S+/g ); // Convert String-formatted options into Object-formatted ones function createOptions( options ) { var object = {}; jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { object[ flag ] = true; } ); return object; } /* * Create a callback list using the following parameters: * * options: an optional list of space-separated options that will change how * the callback list behaves or a more traditional option object * * By default a callback list will act like an event callback list and can be * "fired" multiple times. * * Possible options: * * once: will ensure the callback list can only be fired once (like a Deferred) * * memory: will keep track of previous values and will call any callback added * after the list has been fired right away with the latest "memorized" * values (like a Deferred) * * unique: will ensure a callback can only be added once (no duplicate in the list) * * stopOnFalse: interrupt callings when a callback returns false * */ jQuery.Callbacks = function( options ) { // Convert options from String-formatted to Object-formatted if needed // (we check in cache first) options = typeof options === "string" ? createOptions( options ) : jQuery.extend( {}, options ); var // Flag to know if list is currently firing firing, // Last fire value for non-forgettable lists memory, // Flag to know if list was already fired fired, // Flag to prevent firing locked, // Actual callback list list = [], // Queue of execution data for repeatable lists queue = [], // Index of currently firing callback (modified by add/remove as needed) firingIndex = -1, // Fire callbacks fire = function() { // Enforce single-firing locked = options.once; // Execute callbacks for all pending executions, // respecting firingIndex overrides and runtime changes fired = firing = true; for ( ; queue.length; firingIndex = -1 ) { memory = queue.shift(); while ( ++firingIndex < list.length ) { // Run callback and check for early termination if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && options.stopOnFalse ) { // Jump to end and forget the data so .add doesn't re-fire firingIndex = list.length; memory = false; } } } // Forget the data if we're done with it if ( !options.memory ) { memory = false; } firing = false; // Clean up if we're done firing for good if ( locked ) { // Keep an empty list if we have data for future add calls if ( memory ) { list = []; // Otherwise, this object is spent } else { list = ""; } } }, // Actual Callbacks object self = { // Add a callback or a collection of callbacks to the list add: function() { if ( list ) { // If we have memory from a past run, we should fire after adding if ( memory && !firing ) { firingIndex = list.length - 1; queue.push( memory ); } ( function add( args ) { jQuery.each( args, function( _, arg ) { if ( jQuery.isFunction( arg ) ) { if ( !options.unique || !self.has( arg ) ) { list.push( arg ); } } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { // Inspect recursively add( arg ); } } ); } )( arguments ); if ( memory && !firing ) { fire(); } } return this; }, // Remove a callback from the list remove: function() { jQuery.each( arguments, function( _, arg ) { var index; while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { list.splice( index, 1 ); // Handle firing indexes if ( index <= firingIndex ) { firingIndex--; } } } ); return this; }, // Check if a given callback is in the list. // If no argument is given, return whether or not list has callbacks attached. has: function( fn ) { return fn ? jQuery.inArray( fn, list ) > -1 : list.length > 0; }, // Remove all callbacks from the list empty: function() { if ( list ) { list = []; } return this; }, // Disable .fire and .add // Abort any current/pending executions // Clear all callbacks and values disable: function() { locked = queue = []; list = memory = ""; return this; }, disabled: function() { return !list; }, // Disable .fire // Also disable .add unless we have memory (since it would have no effect) // Abort any pending executions lock: function() { locked = queue = []; if ( !memory ) { list = memory = ""; } return this; }, locked: function() { return !!locked; }, // Call all callbacks with the given context and arguments fireWith: function( context, args ) { if ( !locked ) { args = args || []; args = [ context, args.slice ? args.slice() : args ]; queue.push( args ); if ( !firing ) { fire(); } } return this; }, // Call all the callbacks with the given arguments fire: function() { self.fireWith( this, arguments ); return this; }, // To know if the callbacks have already been called at least once fired: function() { return !!fired; } }; return self; }; jQuery.extend( { Deferred: function( func ) { var tuples = [ // action, add listener, listener list, final state [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], [ "notify", "progress", jQuery.Callbacks( "memory" ) ] ], state = "pending", promise = { state: function() { return state; }, always: function() { deferred.done( arguments ).fail( arguments ); return this; }, then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; return jQuery.Deferred( function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; // deferred[ done | fail | progress ] for forwarding actions to newDefer deferred[ tuple[ 1 ] ]( function() { var returned = fn && fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { returned.promise() .progress( newDefer.notify ) .done( newDefer.resolve ) .fail( newDefer.reject ); } else { newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); } } ); } ); fns = null; } ).promise(); }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) { return obj != null ? jQuery.extend( obj, promise ) : promise; } }, deferred = {}; // Keep pipe for back-compat promise.pipe = promise.then; // Add list-specific methods jQuery.each( tuples, function( i, tuple ) { var list = tuple[ 2 ], stateString = tuple[ 3 ]; // promise[ done | fail | progress ] = list.add promise[ tuple[ 1 ] ] = list.add; // Handle state if ( stateString ) { list.add( function() { // state = [ resolved | rejected ] state = stateString; // [ reject_list | resolve_list ].disable; progress_list.lock }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); } // deferred[ resolve | reject | notify ] deferred[ tuple[ 0 ] ] = function() { deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments ); return this; }; deferred[ tuple[ 0 ] + "With" ] = list.fireWith; } ); // Make the deferred a promise promise.promise( deferred ); // Call given func if any if ( func ) { func.call( deferred, deferred ); } // All done! return deferred; }, // Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, resolveValues = slice.call( arguments ), length = resolveValues.length, // the count of uncompleted subordinates remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, // the master Deferred. // If resolveValues consist of only a single Deferred, just use that. deferred = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values updateFunc = function( i, contexts, values ) { return function( value ) { contexts[ i ] = this; values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; if ( values === progressValues ) { deferred.notifyWith( contexts, values ); } else if ( !( --remaining ) ) { deferred.resolveWith( contexts, values ); } }; }, progressValues, progressContexts, resolveContexts; // Add listeners to Deferred subordinates; treat others as resolved if ( length > 1 ) { progressValues = new Array( length ); progressContexts = new Array( length ); resolveContexts = new Array( length ); for ( ; i < length; i++ ) { if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { resolveValues[ i ].promise() .progress( updateFunc( i, progressContexts, progressValues ) ) .done( updateFunc( i, resolveContexts, resolveValues ) ) .fail( deferred.reject ); } else { --remaining; } } } // If we're not waiting on anything, resolve the master if ( !remaining ) { deferred.resolveWith( resolveContexts, resolveValues ); } return deferred.promise(); } } ); // The deferred used on DOM ready var readyList; jQuery.fn.ready = function( fn ) { // Add the callback jQuery.ready.promise().done( fn ); return this; }; jQuery.extend( { // Is the DOM ready to be used? Set to true once it occurs. isReady: false, // A counter to track how many items to wait for before // the ready event fires. See #6781 readyWait: 1, // Hold (or release) the ready event holdReady: function( hold ) { if ( hold ) { jQuery.readyWait++; } else { jQuery.ready( true ); } }, // Handle when the DOM is ready ready: function( wait ) { // Abort if there are pending holds or we're already ready if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { return; } // Remember that the DOM is ready jQuery.isReady = true; // If a normal DOM Ready event fired, decrement, and wait if need be if ( wait !== true && --jQuery.readyWait > 0 ) { return; } // If there are functions bound, to execute readyList.resolveWith( document, [ jQuery ] ); // Trigger any bound ready events if ( jQuery.fn.triggerHandler ) { jQuery( document ).triggerHandler( "ready" ); jQuery( document ).off( "ready" ); } } } ); /** * The ready event handler and self cleanup method */ function completed() { document.removeEventListener( "DOMContentLoaded", completed ); window.removeEventListener( "load", completed ); jQuery.ready(); } jQuery.ready.promise = function( obj ) { if ( !readyList ) { readyList = jQuery.Deferred(); // Catch cases where $(document).ready() is called // after the browser event has already occurred. // Support: IE9-10 only // Older IE sometimes signals "interactive" too soon if ( document.readyState === "complete" || ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { // Handle it asynchronously to allow scripts the opportunity to delay ready window.setTimeout( jQuery.ready ); } else { // Use the handy event callback document.addEventListener( "DOMContentLoaded", completed ); // A fallback to window.onload, that will always work window.addEventListener( "load", completed ); } } return readyList.promise( obj ); }; // Kick off the DOM ready check even if the user does not jQuery.ready.promise(); // Multifunctional method to get and set values of a collection // The value/s can optionally be executed if it's a function var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { var i = 0, len = elems.length, bulk = key == null; // Sets many values if ( jQuery.type( key ) === "object" ) { chainable = true; for ( i in key ) { access( elems, fn, i, key[ i ], true, emptyGet, raw ); } // Sets one value } else if ( value !== undefined ) { chainable = true; if ( !jQuery.isFunction( value ) ) { raw = true; } if ( bulk ) { // Bulk operations run against the entire set if ( raw ) { fn.call( elems, value ); fn = null; // ...except when executing function values } else { bulk = fn; fn = function( elem, key, value ) { return bulk.call( jQuery( elem ), value ); }; } } if ( fn ) { for ( ; i < len; i++ ) { fn( elems[ i ], key, raw ? value : value.call( elems[ i ], i, fn( elems[ i ], key ) ) ); } } } return chainable ? elems : // Gets bulk ? fn.call( elems ) : len ? fn( elems[ 0 ], key ) : emptyGet; }; var acceptData = function( owner ) { // Accepts only: // - Node // - Node.ELEMENT_NODE // - Node.DOCUMENT_NODE // - Object // - Any /* jshint -W018 */ return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); }; function Data() { this.expando = jQuery.expando + Data.uid++; } Data.uid = 1; Data.prototype = { register: function( owner, initial ) { var value = initial || {}; // If it is a node unlikely to be stringify-ed or looped over // use plain assignment if ( owner.nodeType ) { owner[ this.expando ] = value; // Otherwise secure it in a non-enumerable, non-writable property // configurability must be true to allow the property to be // deleted with the delete operator } else { Object.defineProperty( owner, this.expando, { value: value, writable: true, configurable: true } ); } return owner[ this.expando ]; }, cache: function( owner ) { // We can accept data for non-element nodes in modern browsers, // but we should not, see #8335. // Always return an empty object. if ( !acceptData( owner ) ) { return {}; } // Check if the owner object already has a cache var value = owner[ this.expando ]; // If not, create one if ( !value ) { value = {}; // We can accept data for non-element nodes in modern browsers, // but we should not, see #8335. // Always return an empty object. if ( acceptData( owner ) ) { // If it is a node unlikely to be stringify-ed or looped over // use plain assignment if ( owner.nodeType ) { owner[ this.expando ] = value; // Otherwise secure it in a non-enumerable property // configurable must be true to allow the property to be // deleted when data is removed } else { Object.defineProperty( owner, this.expando, { value: value, configurable: true } ); } } } return value; }, set: function( owner, data, value ) { var prop, cache = this.cache( owner ); // Handle: [ owner, key, value ] args if ( typeof data === "string" ) { cache[ data ] = value; // Handle: [ owner, { properties } ] args } else { // Copy the properties one-by-one to the cache object for ( prop in data ) { cache[ prop ] = data[ prop ]; } } return cache; }, get: function( owner, key ) { return key === undefined ? this.cache( owner ) : owner[ this.expando ] && owner[ this.expando ][ key ]; }, access: function( owner, key, value ) { var stored; // In cases where either: // // 1. No key was specified // 2. A string key was specified, but no value provided // // Take the "read" path and allow the get method to determine // which value to return, respectively either: // // 1. The entire cache object // 2. The data stored at the key // if ( key === undefined || ( ( key && typeof key === "string" ) && value === undefined ) ) { stored = this.get( owner, key ); return stored !== undefined ? stored : this.get( owner, jQuery.camelCase( key ) ); } // When the key is not a string, or both a key and value // are specified, set or extend (existing objects) with either: // // 1. An object of properties // 2. A key and value // this.set( owner, key, value ); // Since the "set" path can have two possible entry points // return the expected data based on which path was taken[*] return value !== undefined ? value : key; }, remove: function( owner, key ) { var i, name, camel, cache = owner[ this.expando ]; if ( cache === undefined ) { return; } if ( key === undefined ) { this.register( owner ); } else { // Support array or space separated string of keys if ( jQuery.isArray( key ) ) { // If "name" is an array of keys... // When data is initially created, via ("key", "val") signature, // keys will be converted to camelCase. // Since there is no way to tell _how_ a key was added, remove // both plain key and camelCase key. #12786 // This will only penalize the array argument path. name = key.concat( key.map( jQuery.camelCase ) ); } else { camel = jQuery.camelCase( key ); // Try the string as a key before any manipulation if ( key in cache ) { name = [ key, camel ]; } else { // If a key with the spaces exists, use it. // Otherwise, create an array by matching non-whitespace name = camel; name = name in cache ? [ name ] : ( name.match( rnotwhite ) || [] ); } } i = name.length; while ( i-- ) { delete cache[ name[ i ] ]; } } // Remove the expando if there's no more data if ( key === undefined || jQuery.isEmptyObject( cache ) ) { // Support: Chrome <= 35-45+ // Webkit & Blink performance suffers when deleting properties // from DOM nodes, so set to undefined instead // https://code.google.com/p/chromium/issues/detail?id=378607 if ( owner.nodeType ) { owner[ this.expando ] = undefined; } else { delete owner[ this.expando ]; } } }, hasData: function( owner ) { var cache = owner[ this.expando ]; return cache !== undefined && !jQuery.isEmptyObject( cache ); } }; var dataPriv = new Data(); var dataUser = new Data(); // Implementation Summary // // 1. Enforce API surface and semantic compatibility with 1.9.x branch // 2. Improve the module's maintainability by reducing the storage // paths to a single mechanism. // 3. Use the same single mechanism to support "private" and "user" data. // 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) // 5. Avoid exposing implementation details on user objects (eg. expando properties) // 6. Provide a clear path for implementation upgrade to WeakMap in 2014 var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, rmultiDash = /[A-Z]/g; function dataAttr( elem, key, data ) { var name; // If nothing was found internally, try to fetch any // data from the HTML5 data-* attribute if ( data === undefined && elem.nodeType === 1 ) { name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); data = elem.getAttribute( name ); if ( typeof data === "string" ) { try { data = data === "true" ? true : data === "false" ? false : data === "null" ? null : // Only convert to a number if it doesn't change the string +data + "" === data ? +data : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch ( e ) {} // Make sure we set the data so it isn't changed later dataUser.set( elem, key, data ); } else { data = undefined; } } return data; } jQuery.extend( { hasData: function( elem ) { return dataUser.hasData( elem ) || dataPriv.hasData( elem ); }, data: function( elem, name, data ) { return dataUser.access( elem, name, data ); }, removeData: function( elem, name ) { dataUser.remove( elem, name ); }, // TODO: Now that all calls to _data and _removeData have been replaced // with direct calls to dataPriv methods, these can be deprecated. _data: function( elem, name, data ) { return dataPriv.access( elem, name, data ); }, _removeData: function( elem, name ) { dataPriv.remove( elem, name ); } } ); jQuery.fn.extend( { data: function( key, value ) { var i, name, data, elem = this[ 0 ], attrs = elem && elem.attributes; // Gets all values if ( key === undefined ) { if ( this.length ) { data = dataUser.get( elem ); if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { i = attrs.length; while ( i-- ) { // Support: IE11+ // The attrs elements can be null (#14894) if ( attrs[ i ] ) { name = attrs[ i ].name; if ( name.indexOf( "data-" ) === 0 ) { name = jQuery.camelCase( name.slice( 5 ) ); dataAttr( elem, name, data[ name ] ); } } } dataPriv.set( elem, "hasDataAttrs", true ); } } return data; } // Sets multiple values if ( typeof key === "object" ) { return this.each( function() { dataUser.set( this, key ); } ); } return access( this, function( value ) { var data, camelKey; // The calling jQuery object (element matches) is not empty // (and therefore has an element appears at this[ 0 ]) and the // `value` parameter was not undefined. An empty jQuery object // will result in `undefined` for elem = this[ 0 ] which will // throw an exception if an attempt to read a data cache is made. if ( elem && value === undefined ) { // Attempt to get data from the cache // with the key as-is data = dataUser.get( elem, key ) || // Try to find dashed key if it exists (gh-2779) // This is for 2.2.x only dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() ); if ( data !== undefined ) { return data; } camelKey = jQuery.camelCase( key ); // Attempt to get data from the cache // with the key camelized data = dataUser.get( elem, camelKey ); if ( data !== undefined ) { return data; } // Attempt to "discover" the data in // HTML5 custom data-* attrs data = dataAttr( elem, camelKey, undefined ); if ( data !== undefined ) { return data; } // We tried really hard, but the data doesn't exist. return; } // Set the data... camelKey = jQuery.camelCase( key ); this.each( function() { // First, attempt to store a copy or reference of any // data that might've been store with a camelCased key. var data = dataUser.get( this, camelKey ); // For HTML5 data-* attribute interop, we have to // store property names with dashes in a camelCase form. // This might not apply to all properties...* dataUser.set( this, camelKey, value ); // *... In the case of properties that might _actually_ // have dashes, we need to also store a copy of that // unchanged property. if ( key.indexOf( "-" ) > -1 && data !== undefined ) { dataUser.set( this, key, value ); } } ); }, null, value, arguments.length > 1, null, true ); }, removeData: function( key ) { return this.each( function() { dataUser.remove( this, key ); } ); } } ); jQuery.extend( { queue: function( elem, type, data ) { var queue; if ( elem ) { type = ( type || "fx" ) + "queue"; queue = dataPriv.get( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( data ) { if ( !queue || jQuery.isArray( data ) ) { queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); } else { queue.push( data ); } } return queue || []; } }, dequeue: function( elem, type ) { type = type || "fx"; var queue = jQuery.queue( elem, type ), startLength = queue.length, fn = queue.shift(), hooks = jQuery._queueHooks( elem, type ), next = function() { jQuery.dequeue( elem, type ); }; // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { fn = queue.shift(); startLength--; } if ( fn ) { // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { queue.unshift( "inprogress" ); } // Clear up the last queue stop function delete hooks.stop; fn.call( elem, next, hooks ); } if ( !startLength && hooks ) { hooks.empty.fire(); } }, // Not public - generate a queueHooks object, or return the current one _queueHooks: function( elem, type ) { var key = type + "queueHooks"; return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { empty: jQuery.Callbacks( "once memory" ).add( function() { dataPriv.remove( elem, [ type + "queue", key ] ); } ) } ); } } ); jQuery.fn.extend( { queue: function( type, data ) { var setter = 2; if ( typeof type !== "string" ) { data = type; type = "fx"; setter--; } if ( arguments.length < setter ) { return jQuery.queue( this[ 0 ], type ); } return data === undefined ? this : this.each( function() { var queue = jQuery.queue( this, type, data ); // Ensure a hooks for this queue jQuery._queueHooks( this, type ); if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { jQuery.dequeue( this, type ); } } ); }, dequeue: function( type ) { return this.each( function() { jQuery.dequeue( this, type ); } ); }, clearQueue: function( type ) { return this.queue( type || "fx", [] ); }, // Get a promise resolved when queues of a certain type // are emptied (fx is the type by default) promise: function( type, obj ) { var tmp, count = 1, defer = jQuery.Deferred(), elements = this, i = this.length, resolve = function() { if ( !( --count ) ) { defer.resolveWith( elements, [ elements ] ); } }; if ( typeof type !== "string" ) { obj = type; type = undefined; } type = type || "fx"; while ( i-- ) { tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); if ( tmp && tmp.empty ) { count++; tmp.empty.add( resolve ); } } resolve(); return defer.promise( obj ); } } ); var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; var isHidden = function( elem, el ) { // isHidden might be called from jQuery#filter function; // in that case, element will be second argument elem = el || elem; return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); }; function adjustCSS( elem, prop, valueParts, tween ) { var adjusted, scale = 1, maxIterations = 20, currentValue = tween ? function() { return tween.cur(); } : function() { return jQuery.css( elem, prop, "" ); }, initial = currentValue(), unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), // Starting value computation is required for potential unit mismatches initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && rcssNum.exec( jQuery.css( elem, prop ) ); if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { // Trust units reported by jQuery.css unit = unit || initialInUnit[ 3 ]; // Make sure we update the tween properties later on valueParts = valueParts || []; // Iteratively approximate from a nonzero starting point initialInUnit = +initial || 1; do { // If previous iteration zeroed out, double until we get *something*. // Use string for doubling so we don't accidentally see scale as unchanged below scale = scale || ".5"; // Adjust and apply initialInUnit = initialInUnit / scale; jQuery.style( elem, prop, initialInUnit + unit ); // Update scale, tolerating zero or NaN from tween.cur() // Break the loop if scale is unchanged or perfect, or if we've just had enough. } while ( scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations ); } if ( valueParts ) { initialInUnit = +initialInUnit || +initial || 0; // Apply relative offset (+=/-=) if specified adjusted = valueParts[ 1 ] ? initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : +valueParts[ 2 ]; if ( tween ) { tween.unit = unit; tween.start = initialInUnit; tween.end = adjusted; } } return adjusted; } var rcheckableType = ( /^(?:checkbox|radio)$/i ); var rtagName = ( /<([\w:-]+)/ ); var rscriptType = ( /^$|\/(?:java|ecma)script/i ); // We have to close these tags to support XHTML (#13200) var wrapMap = { // Support: IE9 option: [ 1, "" ], // XHTML parsers do not magically insert elements in the // same way that tag soup parsers do. So we cannot shorten // this by omitting or other required elements. thead: [ 1, "", "
" ], col: [ 2, "", "
" ], tr: [ 2, "", "
" ], td: [ 3, "", "
" ], _default: [ 0, "", "" ] }; // Support: IE9 wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; function getAll( context, tag ) { // Support: IE9-11+ // Use typeof to avoid zero-argument method invocation on host objects (#15151) var ret = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName( tag || "*" ) : typeof context.querySelectorAll !== "undefined" ? context.querySelectorAll( tag || "*" ) : []; return tag === undefined || tag && jQuery.nodeName( context, tag ) ? jQuery.merge( [ context ], ret ) : ret; } // Mark scripts as having already been evaluated function setGlobalEval( elems, refElements ) { var i = 0, l = elems.length; for ( ; i < l; i++ ) { dataPriv.set( elems[ i ], "globalEval", !refElements || dataPriv.get( refElements[ i ], "globalEval" ) ); } } var rhtml = /<|&#?\w+;/; function buildFragment( elems, context, scripts, selection, ignored ) { var elem, tmp, tag, wrap, contains, j, fragment = context.createDocumentFragment(), nodes = [], i = 0, l = elems.length; for ( ; i < l; i++ ) { elem = elems[ i ]; if ( elem || elem === 0 ) { // Add nodes directly if ( jQuery.type( elem ) === "object" ) { // Support: Android<4.1, PhantomJS<2 // push.apply(_, arraylike) throws on ancient WebKit jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); // Convert non-html into a text node } else if ( !rhtml.test( elem ) ) { nodes.push( context.createTextNode( elem ) ); // Convert html into DOM nodes } else { tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); // Deserialize a standard representation tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); wrap = wrapMap[ tag ] || wrapMap._default; tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; // Descend through wrappers to the right content j = wrap[ 0 ]; while ( j-- ) { tmp = tmp.lastChild; } // Support: Android<4.1, PhantomJS<2 // push.apply(_, arraylike) throws on ancient WebKit jQuery.merge( nodes, tmp.childNodes ); // Remember the top-level container tmp = fragment.firstChild; // Ensure the created nodes are orphaned (#12392) tmp.textContent = ""; } } } // Remove wrapper from fragment fragment.textContent = ""; i = 0; while ( ( elem = nodes[ i++ ] ) ) { // Skip elements already in the context collection (trac-4087) if ( selection && jQuery.inArray( elem, selection ) > -1 ) { if ( ignored ) { ignored.push( elem ); } continue; } contains = jQuery.contains( elem.ownerDocument, elem ); // Append to fragment tmp = getAll( fragment.appendChild( elem ), "script" ); // Preserve script evaluation history if ( contains ) { setGlobalEval( tmp ); } // Capture executables if ( scripts ) { j = 0; while ( ( elem = tmp[ j++ ] ) ) { if ( rscriptType.test( elem.type || "" ) ) { scripts.push( elem ); } } } } return fragment; } ( function() { var fragment = document.createDocumentFragment(), div = fragment.appendChild( document.createElement( "div" ) ), input = document.createElement( "input" ); // Support: Android 4.0-4.3, Safari<=5.1 // Check state lost if the name is set (#11217) // Support: Windows Web Apps (WWA) // `name` and `type` must use .setAttribute for WWA (#14901) input.setAttribute( "type", "radio" ); input.setAttribute( "checked", "checked" ); input.setAttribute( "name", "t" ); div.appendChild( input ); // Support: Safari<=5.1, Android<4.2 // Older WebKit doesn't clone checked state correctly in fragments support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; // Support: IE<=11+ // Make sure textarea (and checkbox) defaultValue is properly cloned div.innerHTML = ""; support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; } )(); var rkeyEvent = /^key/, rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, rtypenamespace = /^([^.]*)(?:\.(.+)|)/; function returnTrue() { return true; } function returnFalse() { return false; } // Support: IE9 // See #13393 for more info function safeActiveElement() { try { return document.activeElement; } catch ( err ) { } } function on( elem, types, selector, data, fn, one ) { var origFn, type; // Types can be a map of types/handlers if ( typeof types === "object" ) { // ( types-Object, selector, data ) if ( typeof selector !== "string" ) { // ( types-Object, data ) data = data || selector; selector = undefined; } for ( type in types ) { on( elem, type, selector, data, types[ type ], one ); } return elem; } if ( data == null && fn == null ) { // ( types, fn ) fn = selector; data = selector = undefined; } else if ( fn == null ) { if ( typeof selector === "string" ) { // ( types, selector, fn ) fn = data; data = undefined; } else { // ( types, data, fn ) fn = data; data = selector; selector = undefined; } } if ( fn === false ) { fn = returnFalse; } else if ( !fn ) { return elem; } if ( one === 1 ) { origFn = fn; fn = function( event ) { // Can use an empty set, since event contains the info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } return elem.each( function() { jQuery.event.add( this, types, fn, data, selector ); } ); } /* * Helper functions for managing events -- not part of the public interface. * Props to Dean Edwards' addEvent library for many of the ideas. */ jQuery.event = { global: {}, add: function( elem, types, handler, data, selector ) { var handleObjIn, eventHandle, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, elemData = dataPriv.get( elem ); // Don't attach events to noData or text/comment nodes (but allow plain objects) if ( !elemData ) { return; } // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; selector = handleObjIn.selector; } // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { handler.guid = jQuery.guid++; } // Init the element's event structure and main handler, if this is the first if ( !( events = elemData.events ) ) { events = elemData.events = {}; } if ( !( eventHandle = elemData.handle ) ) { eventHandle = elemData.handle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? jQuery.event.dispatch.apply( elem, arguments ) : undefined; }; } // Handle multiple events separated by a space types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { tmp = rtypenamespace.exec( types[ t ] ) || []; type = origType = tmp[ 1 ]; namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); // There *must* be a type, no attaching namespace-only handlers if ( !type ) { continue; } // If event changes its type, use the special event handlers for the changed type special = jQuery.event.special[ type ] || {}; // If selector defined, determine special event api type, otherwise given type type = ( selector ? special.delegateType : special.bindType ) || type; // Update special based on newly reset type special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers handleObj = jQuery.extend( { type: type, origType: origType, data: data, handler: handler, guid: handler.guid, selector: selector, needsContext: selector && jQuery.expr.match.needsContext.test( selector ), namespace: namespaces.join( "." ) }, handleObjIn ); // Init the event handler queue if we're the first if ( !( handlers = events[ type ] ) ) { handlers = events[ type ] = []; handlers.delegateCount = 0; // Only use addEventListener if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle ); } } } if ( special.add ) { special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // Add to the element's handler list, delegates in front if ( selector ) { handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { handlers.push( handleObj ); } // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } }, // Detach an event or set of events from an element remove: function( elem, types, handler, selector, mappedTypes ) { var j, origCount, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); if ( !elemData || !( events = elemData.events ) ) { return; } // Once for each type.namespace in types; type may be omitted types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { tmp = rtypenamespace.exec( types[ t ] ) || []; type = origType = tmp[ 1 ]; namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); // Unbind all events (on this namespace, if provided) for the element if ( !type ) { for ( type in events ) { jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } continue; } special = jQuery.event.special[ type ] || {}; type = ( selector ? special.delegateType : special.bindType ) || type; handlers = events[ type ] || []; tmp = tmp[ 2 ] && new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); // Remove matching events origCount = j = handlers.length; while ( j-- ) { handleObj = handlers[ j ]; if ( ( mappedTypes || origType === handleObj.origType ) && ( !handler || handler.guid === handleObj.guid ) && ( !tmp || tmp.test( handleObj.namespace ) ) && ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { handlers.splice( j, 1 ); if ( handleObj.selector ) { handlers.delegateCount--; } if ( special.remove ) { special.remove.call( elem, handleObj ); } } } // Remove generic event handler if we removed something and no more handlers exist // (avoids potential for endless recursion during removal of special event handlers) if ( origCount && !handlers.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } delete events[ type ]; } } // Remove data and the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { dataPriv.remove( elem, "handle events" ); } }, dispatch: function( event ) { // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event ); var i, j, ret, matched, handleObj, handlerQueue = [], args = slice.call( arguments ), handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], special = jQuery.event.special[ event.type ] || {}; // Use the fix-ed jQuery.Event rather than the (read-only) native event args[ 0 ] = event; event.delegateTarget = this; // Call the preDispatch hook for the mapped type, and let it bail if desired if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } // Determine handlers handlerQueue = jQuery.event.handlers.call( this, event, handlers ); // Run delegates first; they may want to stop propagation beneath us i = 0; while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { event.currentTarget = matched.elem; j = 0; while ( ( handleObj = matched.handlers[ j++ ] ) && !event.isImmediatePropagationStopped() ) { // Triggered event must either 1) have no namespace, or 2) have namespace(s) // a subset or equal to those in the bound event (both can have no namespace). if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || handleObj.handler ).apply( matched.elem, args ); if ( ret !== undefined ) { if ( ( event.result = ret ) === false ) { event.preventDefault(); event.stopPropagation(); } } } } } // Call the postDispatch hook for the mapped type if ( special.postDispatch ) { special.postDispatch.call( this, event ); } return event.result; }, handlers: function( event, handlers ) { var i, matches, sel, handleObj, handlerQueue = [], delegateCount = handlers.delegateCount, cur = event.target; // Support (at least): Chrome, IE9 // Find delegate handlers // Black-hole SVG instance trees (#13180) // // Support: Firefox<=42+ // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) if ( delegateCount && cur.nodeType && ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { for ( ; cur !== this; cur = cur.parentNode || this ) { // Don't check non-elements (#13208) // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { matches = []; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; // Don't conflict with Object.prototype properties (#13203) sel = handleObj.selector + " "; if ( matches[ sel ] === undefined ) { matches[ sel ] = handleObj.needsContext ? jQuery( sel, this ).index( cur ) > -1 : jQuery.find( sel, this, null, [ cur ] ).length; } if ( matches[ sel ] ) { matches.push( handleObj ); } } if ( matches.length ) { handlerQueue.push( { elem: cur, handlers: matches } ); } } } } // Add the remaining (directly-bound) handlers if ( delegateCount < handlers.length ) { handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); } return handlerQueue; }, // Includes some event props shared by KeyEvent and MouseEvent props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), fixHooks: {}, keyHooks: { props: "char charCode key keyCode".split( " " ), filter: function( event, original ) { // Add which for key events if ( event.which == null ) { event.which = original.charCode != null ? original.charCode : original.keyCode; } return event; } }, mouseHooks: { props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " + "screenX screenY toElement" ).split( " " ), filter: function( event, original ) { var eventDoc, doc, body, button = original.button; // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == null && original.clientX != null ) { eventDoc = event.target.ownerDocument || document; doc = eventDoc.documentElement; body = eventDoc.body; event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } // Add which for click: 1 === left; 2 === middle; 3 === right // Note: button is not normalized, so don't use it if ( !event.which && button !== undefined ) { event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); } return event; } }, fix: function( event ) { if ( event[ jQuery.expando ] ) { return event; } // Create a writable copy of the event object and normalize some properties var i, prop, copy, type = event.type, originalEvent = event, fixHook = this.fixHooks[ type ]; if ( !fixHook ) { this.fixHooks[ type ] = fixHook = rmouseEvent.test( type ) ? this.mouseHooks : rkeyEvent.test( type ) ? this.keyHooks : {}; } copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; event = new jQuery.Event( originalEvent ); i = copy.length; while ( i-- ) { prop = copy[ i ]; event[ prop ] = originalEvent[ prop ]; } // Support: Cordova 2.5 (WebKit) (#13255) // All events should have a target; Cordova deviceready doesn't if ( !event.target ) { event.target = document; } // Support: Safari 6.0+, Chrome<28 // Target should not be a text node (#504, #13143) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; }, special: { load: { // Prevent triggered image.load events from bubbling to window.load noBubble: true }, focus: { // Fire native event if possible so blur/focus sequence is correct trigger: function() { if ( this !== safeActiveElement() && this.focus ) { this.focus(); return false; } }, delegateType: "focusin" }, blur: { trigger: function() { if ( this === safeActiveElement() && this.blur ) { this.blur(); return false; } }, delegateType: "focusout" }, click: { // For checkbox, fire native event so checked state will be right trigger: function() { if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { this.click(); return false; } }, // For cross-browser consistency, don't fire native .click() on links _default: function( event ) { return jQuery.nodeName( event.target, "a" ); } }, beforeunload: { postDispatch: function( event ) { // Support: Firefox 20+ // Firefox doesn't alert if the returnValue field is not set. if ( event.result !== undefined && event.originalEvent ) { event.originalEvent.returnValue = event.result; } } } } }; jQuery.removeEvent = function( elem, type, handle ) { // This "if" is needed for plain objects if ( elem.removeEventListener ) { elem.removeEventListener( type, handle ); } }; jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword if ( !( this instanceof jQuery.Event ) ) { return new jQuery.Event( src, props ); } // Event object if ( src && src.type ) { this.originalEvent = src; this.type = src.type; // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. this.isDefaultPrevented = src.defaultPrevented || src.defaultPrevented === undefined && // Support: Android<4.0 src.returnValue === false ? returnTrue : returnFalse; // Event type } else { this.type = src; } // Put explicitly provided properties onto the event object if ( props ) { jQuery.extend( this, props ); } // Create a timestamp if incoming event doesn't have one this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed this[ jQuery.expando ] = true; }; // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html jQuery.Event.prototype = { constructor: jQuery.Event, isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse, isSimulated: false, preventDefault: function() { var e = this.originalEvent; this.isDefaultPrevented = returnTrue; if ( e && !this.isSimulated ) { e.preventDefault(); } }, stopPropagation: function() { var e = this.originalEvent; this.isPropagationStopped = returnTrue; if ( e && !this.isSimulated ) { e.stopPropagation(); } }, stopImmediatePropagation: function() { var e = this.originalEvent; this.isImmediatePropagationStopped = returnTrue; if ( e && !this.isSimulated ) { e.stopImmediatePropagation(); } this.stopPropagation(); } }; // Create mouseenter/leave events using mouseover/out and event-time checks // so that event delegation works in jQuery. // Do the same for pointerenter/pointerleave and pointerover/pointerout // // Support: Safari 7 only // Safari sends mouseenter too often; see: // https://code.google.com/p/chromium/issues/detail?id=470258 // for the description of the bug (it existed in older Chrome versions as well). jQuery.each( { mouseenter: "mouseover", mouseleave: "mouseout", pointerenter: "pointerover", pointerleave: "pointerout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { delegateType: fix, bindType: fix, handle: function( event ) { var ret, target = this, related = event.relatedTarget, handleObj = event.handleObj; // For mouseenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { event.type = handleObj.origType; ret = handleObj.handler.apply( this, arguments ); event.type = fix; } return ret; } }; } ); jQuery.fn.extend( { on: function( types, selector, data, fn ) { return on( this, types, selector, data, fn ); }, one: function( types, selector, data, fn ) { return on( this, types, selector, data, fn, 1 ); }, off: function( types, selector, fn ) { var handleObj, type; if ( types && types.preventDefault && types.handleObj ) { // ( event ) dispatched jQuery.Event handleObj = types.handleObj; jQuery( types.delegateTarget ).off( handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, handleObj.selector, handleObj.handler ); return this; } if ( typeof types === "object" ) { // ( types-object [, selector] ) for ( type in types ) { this.off( type, selector, types[ type ] ); } return this; } if ( selector === false || typeof selector === "function" ) { // ( types [, fn] ) fn = selector; selector = undefined; } if ( fn === false ) { fn = returnFalse; } return this.each( function() { jQuery.event.remove( this, types, fn, selector ); } ); } } ); var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, // Support: IE 10-11, Edge 10240+ // In IE/Edge using regex groups here causes severe slowdowns. // See https://connect.microsoft.com/IE/feedback/details/1736512/ rnoInnerhtml = /\s*$/g; // Manipulating tables requires a tbody function manipulationTarget( elem, content ) { return jQuery.nodeName( elem, "table" ) && jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? elem.getElementsByTagName( "tbody" )[ 0 ] || elem.appendChild( elem.ownerDocument.createElement( "tbody" ) ) : elem; } // Replace/restore the type attribute of script elements for safe DOM manipulation function disableScript( elem ) { elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; return elem; } function restoreScript( elem ) { var match = rscriptTypeMasked.exec( elem.type ); if ( match ) { elem.type = match[ 1 ]; } else { elem.removeAttribute( "type" ); } return elem; } function cloneCopyEvent( src, dest ) { var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; if ( dest.nodeType !== 1 ) { return; } // 1. Copy private data: events, handlers, etc. if ( dataPriv.hasData( src ) ) { pdataOld = dataPriv.access( src ); pdataCur = dataPriv.set( dest, pdataOld ); events = pdataOld.events; if ( events ) { delete pdataCur.handle; pdataCur.events = {}; for ( type in events ) { for ( i = 0, l = events[ type ].length; i < l; i++ ) { jQuery.event.add( dest, type, events[ type ][ i ] ); } } } } // 2. Copy user data if ( dataUser.hasData( src ) ) { udataOld = dataUser.access( src ); udataCur = jQuery.extend( {}, udataOld ); dataUser.set( dest, udataCur ); } } // Fix IE bugs, see support tests function fixInput( src, dest ) { var nodeName = dest.nodeName.toLowerCase(); // Fails to persist the checked state of a cloned checkbox or radio button. if ( nodeName === "input" && rcheckableType.test( src.type ) ) { dest.checked = src.checked; // Fails to return the selected option to the default selected state when cloning options } else if ( nodeName === "input" || nodeName === "textarea" ) { dest.defaultValue = src.defaultValue; } } function domManip( collection, args, callback, ignored ) { // Flatten any nested arrays args = concat.apply( [], args ); var fragment, first, scripts, hasScripts, node, doc, i = 0, l = collection.length, iNoClone = l - 1, value = args[ 0 ], isFunction = jQuery.isFunction( value ); // We can't cloneNode fragments that contain checked, in WebKit if ( isFunction || ( l > 1 && typeof value === "string" && !support.checkClone && rchecked.test( value ) ) ) { return collection.each( function( index ) { var self = collection.eq( index ); if ( isFunction ) { args[ 0 ] = value.call( this, index, self.html() ); } domManip( self, args, callback, ignored ); } ); } if ( l ) { fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); first = fragment.firstChild; if ( fragment.childNodes.length === 1 ) { fragment = first; } // Require either new content or an interest in ignored elements to invoke the callback if ( first || ignored ) { scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); hasScripts = scripts.length; // Use the original fragment for the last item // instead of the first because it can end up // being emptied incorrectly in certain situations (#8070). for ( ; i < l; i++ ) { node = fragment; if ( i !== iNoClone ) { node = jQuery.clone( node, true, true ); // Keep references to cloned scripts for later restoration if ( hasScripts ) { // Support: Android<4.1, PhantomJS<2 // push.apply(_, arraylike) throws on ancient WebKit jQuery.merge( scripts, getAll( node, "script" ) ); } } callback.call( collection[ i ], node, i ); } if ( hasScripts ) { doc = scripts[ scripts.length - 1 ].ownerDocument; // Reenable scripts jQuery.map( scripts, restoreScript ); // Evaluate executable scripts on first document insertion for ( i = 0; i < hasScripts; i++ ) { node = scripts[ i ]; if ( rscriptType.test( node.type || "" ) && !dataPriv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { if ( node.src ) { // Optional AJAX dependency, but won't run scripts if not present if ( jQuery._evalUrl ) { jQuery._evalUrl( node.src ); } } else { jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); } } } } } } return collection; } function remove( elem, selector, keepData ) { var node, nodes = selector ? jQuery.filter( selector, elem ) : elem, i = 0; for ( ; ( node = nodes[ i ] ) != null; i++ ) { if ( !keepData && node.nodeType === 1 ) { jQuery.cleanData( getAll( node ) ); } if ( node.parentNode ) { if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { setGlobalEval( getAll( node, "script" ) ); } node.parentNode.removeChild( node ); } } return elem; } jQuery.extend( { htmlPrefilter: function( html ) { return html.replace( rxhtmlTag, "<$1>" ); }, clone: function( elem, dataAndEvents, deepDataAndEvents ) { var i, l, srcElements, destElements, clone = elem.cloneNode( true ), inPage = jQuery.contains( elem.ownerDocument, elem ); // Fix IE cloning issues if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && !jQuery.isXMLDoc( elem ) ) { // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 destElements = getAll( clone ); srcElements = getAll( elem ); for ( i = 0, l = srcElements.length; i < l; i++ ) { fixInput( srcElements[ i ], destElements[ i ] ); } } // Copy the events from the original to the clone if ( dataAndEvents ) { if ( deepDataAndEvents ) { srcElements = srcElements || getAll( elem ); destElements = destElements || getAll( clone ); for ( i = 0, l = srcElements.length; i < l; i++ ) { cloneCopyEvent( srcElements[ i ], destElements[ i ] ); } } else { cloneCopyEvent( elem, clone ); } } // Preserve script evaluation history destElements = getAll( clone, "script" ); if ( destElements.length > 0 ) { setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); } // Return the cloned set return clone; }, cleanData: function( elems ) { var data, elem, type, special = jQuery.event.special, i = 0; for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { if ( acceptData( elem ) ) { if ( ( data = elem[ dataPriv.expando ] ) ) { if ( data.events ) { for ( type in data.events ) { if ( special[ type ] ) { jQuery.event.remove( elem, type ); // This is a shortcut to avoid jQuery.event.remove's overhead } else { jQuery.removeEvent( elem, type, data.handle ); } } } // Support: Chrome <= 35-45+ // Assign undefined instead of using delete, see Data#remove elem[ dataPriv.expando ] = undefined; } if ( elem[ dataUser.expando ] ) { // Support: Chrome <= 35-45+ // Assign undefined instead of using delete, see Data#remove elem[ dataUser.expando ] = undefined; } } } } } ); jQuery.fn.extend( { // Keep domManip exposed until 3.0 (gh-2225) domManip: domManip, detach: function( selector ) { return remove( this, selector, true ); }, remove: function( selector ) { return remove( this, selector ); }, text: function( value ) { return access( this, function( value ) { return value === undefined ? jQuery.text( this ) : this.empty().each( function() { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { this.textContent = value; } } ); }, null, value, arguments.length ); }, append: function() { return domManip( this, arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { var target = manipulationTarget( this, elem ); target.appendChild( elem ); } } ); }, prepend: function() { return domManip( this, arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { var target = manipulationTarget( this, elem ); target.insertBefore( elem, target.firstChild ); } } ); }, before: function() { return domManip( this, arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this ); } } ); }, after: function() { return domManip( this, arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this.nextSibling ); } } ); }, empty: function() { var elem, i = 0; for ( ; ( elem = this[ i ] ) != null; i++ ) { if ( elem.nodeType === 1 ) { // Prevent memory leaks jQuery.cleanData( getAll( elem, false ) ); // Remove any remaining nodes elem.textContent = ""; } } return this; }, clone: function( dataAndEvents, deepDataAndEvents ) { dataAndEvents = dataAndEvents == null ? false : dataAndEvents; deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; return this.map( function() { return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); } ); }, html: function( value ) { return access( this, function( value ) { var elem = this[ 0 ] || {}, i = 0, l = this.length; if ( value === undefined && elem.nodeType === 1 ) { return elem.innerHTML; } // See if we can take a shortcut and just use innerHTML if ( typeof value === "string" && !rnoInnerhtml.test( value ) && !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { value = jQuery.htmlPrefilter( value ); try { for ( ; i < l; i++ ) { elem = this[ i ] || {}; // Remove element nodes and prevent memory leaks if ( elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem, false ) ); elem.innerHTML = value; } } elem = 0; // If using innerHTML throws an exception, use the fallback method } catch ( e ) {} } if ( elem ) { this.empty().append( value ); } }, null, value, arguments.length ); }, replaceWith: function() { var ignored = []; // Make the changes, replacing each non-ignored context element with the new content return domManip( this, arguments, function( elem ) { var parent = this.parentNode; if ( jQuery.inArray( this, ignored ) < 0 ) { jQuery.cleanData( getAll( this ) ); if ( parent ) { parent.replaceChild( elem, this ); } } // Force callback invocation }, ignored ); } } ); jQuery.each( { appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { var elems, ret = [], insert = jQuery( selector ), last = insert.length - 1, i = 0; for ( ; i <= last; i++ ) { elems = i === last ? this : this.clone( true ); jQuery( insert[ i ] )[ original ]( elems ); // Support: QtWebKit // .get() because push.apply(_, arraylike) throws push.apply( ret, elems.get() ); } return this.pushStack( ret ); }; } ); var iframe, elemdisplay = { // Support: Firefox // We have to pre-define these values for FF (#10227) HTML: "block", BODY: "block" }; /** * Retrieve the actual display of a element * @param {String} name nodeName of the element * @param {Object} doc Document object */ // Called only from within defaultDisplay function actualDisplay( name, doc ) { var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), display = jQuery.css( elem[ 0 ], "display" ); // We don't have any data stored on the element, // so use "detach" method as fast way to get rid of the element elem.detach(); return display; } /** * Try to determine the default display value of an element * @param {String} nodeName */ function defaultDisplay( nodeName ) { var doc = document, display = elemdisplay[ nodeName ]; if ( !display ) { display = actualDisplay( nodeName, doc ); // If the simple way fails, read from inside an iframe if ( display === "none" || !display ) { // Use the already-created iframe if possible iframe = ( iframe || jQuery( " ``` ### API The ``Reveal`` object exposes a JavaScript API for controlling navigation and reading state: ```javascript // Navigation Reveal.slide( indexh, indexv, indexf ); Reveal.left(); Reveal.right(); Reveal.up(); Reveal.down(); Reveal.prev(); Reveal.next(); Reveal.prevFragment(); Reveal.nextFragment(); // Toggle presentation states, optionally pass true/false to force on/off Reveal.toggleOverview(); Reveal.togglePause(); Reveal.toggleAutoSlide(); // Change a config value at runtime Reveal.configure({ controls: true }); // Returns the present configuration options Reveal.getConfig(); // Fetch the current scale of the presentation Reveal.getScale(); // Retrieves the previous and current slide elements Reveal.getPreviousSlide(); Reveal.getCurrentSlide(); Reveal.getIndices(); // { h: 0, v: 0 } } Reveal.getProgress(); // 0-1 Reveal.getTotalSlides(); // Returns the speaker notes for the current slide Reveal.getSlideNotes(); // State checks Reveal.isFirstSlide(); Reveal.isLastSlide(); Reveal.isOverview(); Reveal.isPaused(); Reveal.isAutoSliding(); ``` ### Slide Changed Event A 'slidechanged' event is fired each time the slide is changed (regardless of state). The event object holds the index values of the current slide as well as a reference to the previous and current slide HTML nodes. Some libraries, like MathJax (see [#226](https://github.com/hakimel/reveal.js/issues/226#issuecomment-10261609)), get confused by the transforms and display states of slides. Often times, this can be fixed by calling their update or render function from this callback. ```javascript Reveal.addEventListener( 'slidechanged', function( event ) { // event.previousSlide, event.currentSlide, event.indexh, event.indexv } ); ``` ### Presentation State The presentation's current state can be fetched by using the `getState` method. A state object contains all of the information required to put the presentation back as it was when `getState` was first called. Sort of like a snapshot. It's a simple object that can easily be stringified and persisted or sent over the wire. ```javascript Reveal.slide( 1 ); // we're on slide 1 var state = Reveal.getState(); Reveal.slide( 3 ); // we're on slide 3 Reveal.setState( state ); // we're back on slide 1 ``` ### Slide States If you set ``data-state="somestate"`` on a slide ``
``, "somestate" will be applied as a class on the document element when that slide is opened. This allows you to apply broad style changes to the page based on the active slide. Furthermore you can also listen to these changes in state via JavaScript: ```javascript Reveal.addEventListener( 'somestate', function() { // TODO: Sprinkle magic }, false ); ``` ### Slide Backgrounds Slides are contained within a limited portion of the screen by default to allow them to fit any display and scale uniformly. You can apply full page backgrounds outside of the slide area by adding a ```data-background``` attribute to your ```
``` elements. Four different types of backgrounds are supported: color, image, video and iframe. Below are a few examples. ```html

All CSS color formats are supported, like rgba() or hsl().

This slide will have a full-size background image.

This background image will be sized to 100px and repeated.

Video. Multiple sources can be defined using a comma separated list. Video will loop when the data-background-video-loop attribute is provided.

Embeds a web page as a background. Note that the page won't be interactive.

``` Backgrounds transition using a fade animation by default. This can be changed to a linear sliding transition by passing ```backgroundTransition: 'slide'``` to the ```Reveal.initialize()``` call. Alternatively you can set ```data-background-transition``` on any section with a background to override that specific transition. ### Parallax Background If you want to use a parallax scrolling background, set the first two config properties below when initializing reveal.js (the other two are optional). ```javascript Reveal.initialize({ // Parallax background image parallaxBackgroundImage: '', // e.g. "https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg" // Parallax background size parallaxBackgroundSize: '', // CSS syntax, e.g. "2100px 900px" - currently only pixels are supported (don't use % or auto) // Amount of pixels to move the parallax background per slide step, // a value of 0 disables movement along the given axis // These are optional, if they aren't specified they'll be calculated automatically parallaxBackgroundHorizontal: 200, parallaxBackgroundVertical: 50 }); ``` Make sure that the background size is much bigger than screen size to allow for some scrolling. [View example](http://lab.hakim.se/reveal-js/?parallaxBackgroundImage=https%3A%2F%2Fs3.amazonaws.com%2Fhakim-static%2Freveal-js%2Freveal-parallax-1.jpg¶llaxBackgroundSize=2100px%20900px). ### Slide Transitions The global presentation transition is set using the ```transition``` config value. You can override the global transition for a specific slide by using the ```data-transition``` attribute: ```html

This slide will override the presentation transition and zoom!

Choose from three transition speeds: default, fast or slow!

``` You can also use different in and out transitions for the same slide: ```html
The train goes on …
and on …
and stops.
(Passengers entering and leaving)
And it starts again.
``` ### Internal links It's easy to link between slides. The first example below targets the index of another slide whereas the second targets a slide with an ID attribute (```
```): ```html Link Link ``` You can also add relative navigation links, similar to the built in reveal.js controls, by appending one of the following classes on any element. Note that each element is automatically given an ```enabled``` class when it's a valid navigation route based on the current slide. ```html ``` ### Fragments Fragments are used to highlight individual elements on a slide. Every element with the class ```fragment``` will be stepped through before moving on to the next slide. Here's an example: http://lab.hakim.se/reveal-js/#/fragments The default fragment style is to start out invisible and fade in. This style can be changed by appending a different class to the fragment: ```html

grow

shrink

fade-out

visible only once

blue only once

highlight-red

highlight-green

highlight-blue

``` Multiple fragments can be applied to the same element sequentially by wrapping it, this will fade in the text on the first step and fade it back out on the second. ```html
I'll fade in, then out
``` The display order of fragments can be controlled using the ```data-fragment-index``` attribute. ```html

Appears last

Appears first

Appears second

``` ### Fragment events When a slide fragment is either shown or hidden reveal.js will dispatch an event. Some libraries, like MathJax (see #505), get confused by the initially hidden fragment elements. Often times this can be fixed by calling their update or render function from this callback. ```javascript Reveal.addEventListener( 'fragmentshown', function( event ) { // event.fragment = the fragment DOM element } ); Reveal.addEventListener( 'fragmenthidden', function( event ) { // event.fragment = the fragment DOM element } ); ``` ### Code syntax highlighting By default, Reveal is configured with [highlight.js](https://highlightjs.org/) for code syntax highlighting. Below is an example with clojure code that will be syntax highlighted. When the `data-trim` attribute is present surrounding whitespace is automatically removed. ```html

(def lazy-fib
  (concat
   [0 1]
   ((fn rfib [a b]
        (lazy-cons (+ a b) (rfib b (+ a b)))) 0 1)))
	
``` ### Slide number If you would like to display the page number of the current slide you can do so using the ```slideNumber``` configuration value. ```javascript // Shows the slide number using default formatting Reveal.configure({ slideNumber: true }); // Slide number formatting can be configured using these variables: // "h.v": horizontal . vertical slide number (default) // "h/v": horizontal / vertical slide number // "c": flattened slide number // "c/t": flattened slide number / total slides Reveal.configure({ slideNumber: 'c/t' }); ``` ### Overview mode Press "Esc" or "o" keys to toggle the overview mode on and off. While you're in this mode, you can still navigate between slides, as if you were at 1,000 feet above your presentation. The overview mode comes with a few API hooks: ```javascript Reveal.addEventListener( 'overviewshown', function( event ) { /* ... */ } ); Reveal.addEventListener( 'overviewhidden', function( event ) { /* ... */ } ); // Toggle the overview mode programmatically Reveal.toggleOverview(); ``` ### Fullscreen mode Just press »F« on your keyboard to show your presentation in fullscreen mode. Press the »ESC« key to exit fullscreen mode. ### Embedded media Embedded HTML5 `