From 8c98965d28578a0e7a3f7313cf0a1aed5582b8e4 Mon Sep 17 00:00:00 2001 From: Olivia Tournois Date: Thu, 18 Oct 2018 16:06:52 +0200 Subject: [PATCH] feat(stark-demo): creation of a getting started page ISSUES CLOSED: #720 --- .../assets/styles/_media-queries.scss | 2 + .../assets/styles/_old-variables.scss | 1 + showcase/src/app/app.component.ts | 2 +- showcase/src/app/app.module.ts | 2 + showcase/src/app/app.routes.ts | 7 + .../_getting-started-theme.scss | 30 +++ .../_getting-started.component.scss | 59 +++++ .../getting-started.component.html | 219 ++++++++++++++++++ .../getting-started.component.ts | 59 +++++ .../getting-started-component/index.ts | 2 + .../starter-structure.ts | 142 ++++++++++++ .../getting-started/getting-started.module.ts | 13 ++ showcase/src/app/getting-started/index.ts | 2 + .../example-viewer/_example-viewer-theme.scss | 2 +- showcase/src/app/shared/index.ts | 1 + showcase/src/app/shared/shared.module.ts | 7 +- .../_table-of-contents-theme.scss | 7 + .../src/app/shared/table-of-contents/index.ts | 2 + .../table-of-content-link.intf.ts | 29 +++ .../table-of-contents.component.html | 11 + .../table-of-contents.component.scss | 59 +++++ .../table-of-contents.component.spec.ts | 84 +++++++ .../table-of-contents.component.ts | 164 +++++++++++++ showcase/src/styles/_theme.scss | 2 + showcase/src/styles/styles.scss | 2 + 25 files changed, 905 insertions(+), 5 deletions(-) create mode 100644 showcase/src/app/getting-started/getting-started-component/_getting-started-theme.scss create mode 100644 showcase/src/app/getting-started/getting-started-component/_getting-started.component.scss create mode 100644 showcase/src/app/getting-started/getting-started-component/getting-started.component.html create mode 100644 showcase/src/app/getting-started/getting-started-component/getting-started.component.ts create mode 100644 showcase/src/app/getting-started/getting-started-component/index.ts create mode 100644 showcase/src/app/getting-started/getting-started-component/starter-structure.ts create mode 100644 showcase/src/app/getting-started/getting-started.module.ts create mode 100644 showcase/src/app/getting-started/index.ts create mode 100644 showcase/src/app/shared/table-of-contents/_table-of-contents-theme.scss create mode 100644 showcase/src/app/shared/table-of-contents/index.ts create mode 100644 showcase/src/app/shared/table-of-contents/table-of-content-link.intf.ts create mode 100644 showcase/src/app/shared/table-of-contents/table-of-contents.component.html create mode 100644 showcase/src/app/shared/table-of-contents/table-of-contents.component.scss create mode 100644 showcase/src/app/shared/table-of-contents/table-of-contents.component.spec.ts create mode 100644 showcase/src/app/shared/table-of-contents/table-of-contents.component.ts diff --git a/packages/stark-ui/assets/styles/_media-queries.scss b/packages/stark-ui/assets/styles/_media-queries.scss index 5ba25d29e1..d9b86d91f9 100644 --- a/packages/stark-ui/assets/styles/_media-queries.scss +++ b/packages/stark-ui/assets/styles/_media-queries.scss @@ -7,3 +7,5 @@ $mobile-only-query: "(max-width: 599px)"; $mobile-only-screen-query: "screen and (max-width: 599px)"; $tablet-only-query: "(min-width: 600px) and (max-width: 959px)"; $tablet-only-screen-query: "screen and (min-width: 600px) and (max-width: 959px)"; +$handset-toc-query-screen:"(min-width: 0px) and (max-width: 839px)"; +$tablet-toc-query-screen:"(min-width: 840px) and (max-width: 1279px)"; diff --git a/packages/stark-ui/assets/styles/_old-variables.scss b/packages/stark-ui/assets/styles/_old-variables.scss index 39d40e42e5..2dddebae95 100644 --- a/packages/stark-ui/assets/styles/_old-variables.scss +++ b/packages/stark-ui/assets/styles/_old-variables.scss @@ -23,6 +23,7 @@ $stark-footer-size: 24px; $primary-dark-text-color: map-get($mat-light-theme-foreground, base); $secondary-dark-text-color: rgba(0, 0, 0, 0.7); $tertiary-dark-text-color: rgba(0, 0, 0, 0.45); +$custom-dark-background: rgba(39, 40, 34, 1); $disabled-color: map-get($mat-light-theme-foreground, disabled-text); $divider-color: map-get($mat-light-theme-foreground, divider); diff --git a/showcase/src/app/app.component.ts b/showcase/src/app/app.component.ts index 5eb3fab4cb..ad734ace82 100644 --- a/showcase/src/app/app.component.ts +++ b/showcase/src/app/app.component.ts @@ -43,7 +43,7 @@ export class AppComponent implements OnInit { label: "Getting started", isVisible: true, isEnabled: true, - targetState: "#" + targetState: "getting-started" }, { id: "menu-news", diff --git a/showcase/src/app/app.module.ts b/showcase/src/app/app.module.ts index 6179692525..d4b5e093b5 100644 --- a/showcase/src/app/app.module.ts +++ b/showcase/src/app/app.module.ts @@ -84,6 +84,7 @@ import { APP_STATES } from "./app.routes"; import { AppComponent } from "./app.component"; import { HomeComponent } from "./home"; import { NoContentComponent } from "./no-content"; +import { GettingStartedModule } from "./getting-started"; /* tslint:disable:no-import-side-effect */ // load PostCSS styles import "../styles/styles.pcss"; @@ -221,6 +222,7 @@ export const metaReducers: MetaReducer[] = ENV !== "production" ? [logger } }), SharedModule, + GettingStartedModule, StarkAppFooterModule, StarkAppLogoModule, StarkAppLogoutModule, diff --git a/showcase/src/app/app.routes.ts b/showcase/src/app/app.routes.ts index f33326aaba..8ba6899e61 100644 --- a/showcase/src/app/app.routes.ts +++ b/showcase/src/app/app.routes.ts @@ -2,6 +2,7 @@ import { HomeComponent } from "./home"; import { NoContentComponent } from "./no-content"; import { Ng2StateDeclaration } from "@uirouter/angular"; import { AppComponent } from "./app.component"; +import { GettingStartedComponent } from "./getting-started/getting-started-component"; export const APP_STATES: Ng2StateDeclaration[] = [ { @@ -15,6 +16,12 @@ export const APP_STATES: Ng2StateDeclaration[] = [ views: { "@": { component: HomeComponent } }, parent: "app" }, + { + name: "getting-started", + url: "^/getting-started", + views: { "@": { component: GettingStartedComponent } }, + parent: "app" + }, { name: "news.**", url: "^/news", // use ^ to avoid double slash "//" in the URL after the domain (https://github.com/angular-ui/ui-router/wiki/URL-Routing#absolute-routes-) diff --git a/showcase/src/app/getting-started/getting-started-component/_getting-started-theme.scss b/showcase/src/app/getting-started/getting-started-component/_getting-started-theme.scss new file mode 100644 index 0000000000..f9b9bd7089 --- /dev/null +++ b/showcase/src/app/getting-started/getting-started-component/_getting-started-theme.scss @@ -0,0 +1,30 @@ +.file-structure-title { + color: mat-color($grey-palette, 900); +} + +.custom-title { + color: mat-color($grey-palette, 900); +} + +.custom-subtitle { + color: mat-color($grey-palette, 800); +} + +.custom-code { + font-family: monospace; + background-color: mat-color($grey-palette, 200); +} + +.custom-section { + & a { + text-decoration: none; + } +} + +.custom-pretty-print { + & pre { + background-color: $custom-dark-background; + border-radius: 0.3em; + } + color: mat-color($grey-palette, 100); +} diff --git a/showcase/src/app/getting-started/getting-started-component/_getting-started.component.scss b/showcase/src/app/getting-started/getting-started-component/_getting-started.component.scss new file mode 100644 index 0000000000..e01a39215c --- /dev/null +++ b/showcase/src/app/getting-started/getting-started-component/_getting-started.component.scss @@ -0,0 +1,59 @@ +.getting-started-content { + padding-right: 200px; +} + +.file-structure-title { + font-size: 21px; + padding-bottom: 0; + display: inline-flex; + vertical-align: middle; + margin: 10px 0 10px 0; +} + +.structure-description-margin { + margin-bottom: 20px; + margin-top: 20px; +} + +.description { + align-self: center; +} + +@media #{$mobile-only-query} { + .getting-started-content { + padding-right: 0; + } +} + +.custom-title { + font-size: 27px; + display: block; + vertical-align: middle; + margin: 40px 0 10px 0; +} + +.custom-subtitle { + font-size: 20px; + display: block; + vertical-align: middle; + margin: 15px 0 10px 0; +} + +.custom-code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + border-radius: 3px; +} + +.table-of-contents-level-h2 { + padding-left: 12px; +} + +.custom-pretty-print { + & pre { + white-space: pre; + overflow-x: scroll; + font-size: 13px; + } +} diff --git a/showcase/src/app/getting-started/getting-started-component/getting-started.component.html b/showcase/src/app/getting-started/getting-started-component/getting-started.component.html new file mode 100644 index 0000000000..b377fe8fe6 --- /dev/null +++ b/showcase/src/app/getting-started/getting-started-component/getting-started.component.html @@ -0,0 +1,219 @@ +
+
Getting started
+ + + +
+

How to install Stark ?

+

To install Stark, you have to make sure your system is well configured, and then simply install the packages + you need.

+

System Configuration

+

Ensure that you are running the latest version:

+
    +
  • Node 8.x.x +
  • +
  • NPM 5.8.x +
  • +
+

If you have nvm installed, which is highly recommended, you can do a + nvm install --lts && nvm use in $ + to run with the latest Node LTS. You can also have thiszsh done for you + automatically.

+ +

Packages installation

+ +

To install the different packages of the Stark framework, for example, the stark-core package, just do:

+ npm install "@nationalbankbelgium/stark-core" +

Those packages are available at the following locations in NPM:

+ +
+ +
+

Using the Stark Starter

+ +

The Starter project is an empty project using the Stark framework, that you can adapt as you want.

+

To do so, you'll have to simply clone the repository, adapt your configuration if needed and then, run it + once you've made the changes you want.

+

Of course, you'll be able to run tests as well, such as unit tests and E2E tests.

+ +

Clone the repository

+ +

To clone the project, use the following command line:

+ +

Then, go to the starter folder (cd starter) and install all dependencies + using: + npm install

+ +

Adapt the configuration

+

Most of the configuration files at the root of the starter either fully reuse or extend Stark's configuration + files.

+

Most of the time you won't need to change these, but they allow you to customize things when needed.

+

Here is the file structure for the Starter project:

+ + +

Running the application

+

Have you already installed all dependencies? Then you can run the app! Run npm run server to start a local (development) server using webpack-dev-server which will watch, + build (in-memory), and reload for you. The port will be displayed to you as http;//0.0.0.0:3000 + (or if you prefer IPv6, if you're using express server, then it is + http://[::1]:3000).

+

You may enable Hot Module Replacement (HMR) using:

+ + +

You may also start the server in production mode using the following

+ + +

To build the files themselves, simply use:

+ + +

To run the watch mode:

+ + +

Unit testing

+ +

Simply enter the following command line:

+ +

And you can also enable the watch mode for the following

+ + +

E2E testing

+

The command line to execute E2E tests is similar than the one for unit tests:

+ + +

Be aware that you can execute both unit tests and E2E tests with the same command line:

+ +
+ +
+

Good to know

+

AoT Don'ts

+

In the following list are some actions that will make AoT compile fail:

+
    +
  • Don’t use require statements for your templates or styles, use styleUrls and templateUrls, the + angular2-template-loader plugin will change it to require at build time. +
  • +
  • Don’t use default exports.
  • +
  • Don’t use form.controls.controlName, use form.get(‘controlName’) +
  • +
  • Don’t use control.errors?.someError, use control.hasError(‘someError’) +
  • +
  • Don’t use functions in your providers, routes or declarations, export a function and then reference + that + function name +
  • +
  • @Inputs, @Outputs, View or Content Child(ren), Hostbindings, and any field you use from the template + or + annotate for Angular should be public +
  • +
+ +

Type definitions

+

When including 3rd party modules you also need to include the type definition for the module. + When you include a module that doesn't include TypeScript type definitions inside of the module you can + include + external type definitions with @types

+ + +

If you can't find the type definition in the registry then you can make use of an ambient definition in + the + custom-typings.d.ts file.

+

For example:

+ + +

If you're prototyping and you will fix the types later you can also declare it as type any:

+ + +

If you're importing a module that uses Node.js modules which are CommonJS you need to import as

+ + +

External Stylesheets

+

If you wish to import your own stylesheets in the Starter Project, go to the following location in the project:

+ +

In the styles.css file, you can import what you need from other libraries but also + your own styles:

+ +

In that file, it is mandatory to import the stark-styles.scss file, as it is in that file that + the styles from the Stark components will be imported. You can edit that file as well and take only the files that your project + needs. So if you like one of our components but not the style associated to it, you can define your own style for it!

+

The stark-styles.scss file looks like the following;

+ +
+ +
+

FAQ

+ +
    +
  • How do I start the app when I get EACCES and EADDRINUSE + errors? +
      +
    • The EADDRINUSE error means the port 3000 + is currently being used and EACCES is lack of permission + for + webpack to build files to ./dist/
    • +
    +
  • +
  • Error: Cannot find module 'tapable' +
      +
    • Remove node_modules/ and run npm cache clean + then npm install
    • +
    +
  • +
  • How do I turn on Hot Module Replacement +
      +
    • Run npm run server:dev:hmr
    • +
    +
  • +
  • RangeError: Maximum call stack size exceeded +
      +
    • This is a problem with minifying Angular and its recent JIT templates. + If you set mangle to false + then + you should be good +
    • +
    +
  • +
  • Why is the size of my app larger in development? +
      +
    • We are using inline source-maps and hot module replacement which will increase the bundle + size. +
    • +
    +
  • +
  • If you're in China +
      +
    • check out cnpm
    • +
    +
  • +
  • node-pre-gyp ERR in npm install (Windows) +
      +
    • install Python x86 version between 2.5 and 3.0 on windows
    • +
    • or try --no-optional
    • +
    +
  • +
+ +
+
+ + + diff --git a/showcase/src/app/getting-started/getting-started-component/getting-started.component.ts b/showcase/src/app/getting-started/getting-started-component/getting-started.component.ts new file mode 100644 index 0000000000..0921b2cda8 --- /dev/null +++ b/showcase/src/app/getting-started/getting-started-component/getting-started.component.ts @@ -0,0 +1,59 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +import { starterStructure, stylesheetImport, stylesCss, starkStylesCss } from "./starter-structure"; + +@Component({ + selector: "getting-started", + templateUrl: "./getting-started.component.html" +}) +export class GettingStartedComponent implements OnInit { + public constructor(@Inject(STARK_LOGGING_SERVICE) public loggingService: StarkLoggingService) {} + + public stylesheetImport: string = stylesheetImport; + public structure: string = starterStructure; + public stylesCSS: string = stylesCss; + public starkStylesCSS: string = starkStylesCss; + public installingProject: string; + public runningHMR: string; + public runningProject: string; + public buildFiles: string; + public watchMode: string; + public unitTestRunning: string; + public watchAndRunTests: string; + public runEndToEndTests: string; + public continuousIntegrationTests: string; + public installType: string; + public moduleDeclaration: string; + public prototyping: string; + public moduleImport: string; + + public ngOnInit(): void { + this.installingProject = + "# --depth 1 removes all but one .git commit history\n" + + "git clone --depth 1 https://github.com/NationalBankBelgium/stark.git"; + this.runningHMR = "npm run server:dev:hmr"; + this.runningProject = "npm run server:prod"; + this.buildFiles = + "# development\n" + + "npm run build:dev\n\n" + + "# production (jit)\n" + + "npm run build:prod\n\n" + + "# AoT\n" + + "npm run build:aot"; + this.watchMode = "npm run watch"; + this.unitTestRunning = "npm run test"; + this.watchAndRunTests = "npm run watch:test"; + this.runEndToEndTests = + "# update Webdriver (optional, done automatically by postinstall script)\n" + + "npm run webdriver:update # cfr #35\n\n" + + "# this will start a test server and launch Protractor\n" + + "npm run e2e"; + this.continuousIntegrationTests = "# this will test both your JIT and AoT builds\n" + "npm run ci"; + this.installType = "npm install @types/node\n" + "npm install @types/lodash"; + this.moduleDeclaration = 'declare module "my-module" {\n' + "\texport function doesSomething(value: string): string;\n" + "}"; + this.prototyping = "declare var assert: any;\n" + "declare var _: any;\n" + "declare var $: any;"; + this.moduleImport = 'import * as _ from "lodash";'; + + this.loggingService.debug("hello from `GettingStarted` component"); + } +} diff --git a/showcase/src/app/getting-started/getting-started-component/index.ts b/showcase/src/app/getting-started/getting-started-component/index.ts new file mode 100644 index 0000000000..360c27dbd8 --- /dev/null +++ b/showcase/src/app/getting-started/getting-started-component/index.ts @@ -0,0 +1,2 @@ +export * from "./getting-started.component"; +export * from "./starter-structure"; diff --git a/showcase/src/app/getting-started/getting-started-component/starter-structure.ts b/showcase/src/app/getting-started/getting-started-component/starter-structure.ts new file mode 100644 index 0000000000..938432c75d --- /dev/null +++ b/showcase/src/app/getting-started/getting-started-component/starter-structure.ts @@ -0,0 +1,142 @@ +export const starterStructure: string = `| ++---config +| | +| | webpack-custom-config.dev.json # webpack configuration for the development environment +| ++---src +| | +| +---app +| | | +| | +---foo # smart component folder +| | | | # create a folder for each component +| | | | foo.component.css # styles for this smart component +| | | | foo.component.html # template for this smart component +| | | | foo.component.spec.ts # jasmine test for this smart component +| | | | foo.component.ts # configuration metadata for this smart component +| | | | foo.e2e.ts # end-to-end test for this smart component +| | | | index.ts # exports the smart component +| | | | +| | | app.component.css # application base styles +| | | app.component.html # application template +| | | app.component.spec.ts # jasmine test for the app component +| | | app.component.ts # app root component and controller +| | | app.e2e.ts # end-to-end test for the app class +| | | app.module.ts # bootstraps code of the application (configures Angular module, ...) +| | | app.routes.ts # routing configuration +| | | app.services.ts # exports the application state +| | | index.ts # exports the app module +| | | material-icons.config.ts # helper functions for the Material Design icons +| | | routes.config.ts # helper functions for the routing +| | \\ translation.config.ts # helper functions for the translations +| | +| +---assets # static assets (fonts, images, translations, ...) +| | | +| | +---fonts # application-specific fonts +| | | \\ +| | | +| | +---images # application-specific images +| | | | +| | | +---app-icons # application icons for mobile devices +| | | | \\ +| | | | +| | | +---touch # touch icons for mobile devices +| | | | \\ +| | | \\ +| | | +| | +---mock-data # +| | | \\ +| | | +| | +---translations # application-specific translations +| | | | # +| | | | en.json # English +| | | | fr.json # French +| | | \\ nl.json # Dutch +| | | +| | \\ README.md # +| | +| +---assets-base # static assets that will be copied to the root of the application +| | | # +| | | browserconfig.xml # application icons for Windows mobile devices +| | | crossdomain.xml # cross domain policies +| | | favicon.ico # icon for bookmarks bar +| | | humans.txt # contains information about the website (http://humanstxt.org/) +| | | manifest.json # application icons for Android mobile devices +| | | README.md # +| | | robots.txt # the robots exclusion protocol (http://www.robotstxt.org/) +| | \\ service-worker.js # support for building Progressive Web Applications (PWA) with Service Workers +| | +| +---environments # configuration variables for each environment +| | | # +| | | environment.e2e.prod.ts # e2e tests configuration +| | | environment.hmr.ts # development with HMR (Hot Module Replacement) configuration +| | | environment.prod.ts # production configuration +| | | environment.ts # development configuration +| | \\ model.ts # interface defining the variables +| | +| +---styles # application-specific styles +| | \\ +| | +| | custom-typings.d.ts # add your own type definitions that can't be found in the registry in this file +| | hmr.ts # Hot Module Replacement logic +| | index.html # the main HTML page that is served when someone visits this site +| | main.browser.ts # Angular bootstrapping +| | polyfills.browser.ts # this file includes polyfills needed by Angular and should be loaded before the app +| | stark-app-config.json # Stark configuration +| \\ stark-app-metadata.json # Application metadata like: name, description, version... +| +| .dockerignore # files and directories to be excluded by the Docker build +| .gitignore # files and directories to be excluded by git +| .prettierignore # files and directories to be excluded by prettier +| .prettierrc.js # prettier configuration file +| .stylelintrc # stylelint configuration file +| .travis.yml # YAML file to customize the Travis build (https://travis-ci.org/) +| angular.json # Angular configuration file +| base.spec.ts # Initializes the test environment +| Dockerfile # the commands that will be executed by the Docker Build command +| karma.conf.ci.js # Karma configuration file for Continuous Integration +| karma.conf.js # Karma configuration file +| package.json # +| protractor.conf.js # protractor configuration file +| README.md # this document +| tsconfig.app.json # typescript configuration for the application, extends tsconfig.json +| tsconfig.e2e.json # typescript configuration for the e2e tests, extends tsconfig.json +| tsconfig.json # typescript configuration, extends stark-build/tsconfig.json +| tsconfig.spec.json # typescript configuration for the Karma tests, extends tsconfig.json +| tslint.json # tslint configuration file +\\ webpack.config.js # exports the webpack config file from config folder according to the current environment`; + +export const stylesheetImport: string = `| +| +---styles # application-specific styles +| | \ +| | +| | custom-typings.d.ts # add your own type definitions that can'tbe found in the registry in this file +| | hmr.ts # Hot Module Replacement logic +| | index.html # the main HTML page that is served when someone visits this site +| | main.browser.ts # Angular bootstrapping +| | polyfills.browser.ts # this file includes polyfills needed by Angular and should be loaded before the app +| | stark-app-config.json # Stark configuration +| \ stark-app-metadata.json # Application metadata like: name, description, version...`; + +export const stylesCss: string = ` +@import "~basscss/css/basscss.css"; +@import "theme"; +@import "stark-styles.scss"; +@import "../app/app.component.scss"; +`; + +export const starkStylesCss: string = ` +/* Stark styles */ +@import "~@nationalbankbelgium/stark-ui/assets/styles/header"; + +/* Stark components */ +@import "~@nationalbankbelgium/stark-ui/src/modules/app-logo/components/app-logo-theme"; +@import "~@nationalbankbelgium/stark-ui/src/modules/app-logo/components/app-logo.component"; +@import "~@nationalbankbelgium/stark-ui/src/modules/app-footer/components/app-footer.component"; +@import "~@nationalbankbelgium/stark-ui/src/modules/app-footer/components/app-footer-theme"; +@import "~@nationalbankbelgium/stark-ui/src/modules/app-menu/components/app-menu-theme"; + + +/* Stark session-ui pages */ +@import "~@nationalbankbelgium/stark-ui/src/modules/session-ui/pages/login/login-page.component"; +@import "~@nationalbankbelgium/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component"; +`; diff --git a/showcase/src/app/getting-started/getting-started.module.ts b/showcase/src/app/getting-started/getting-started.module.ts new file mode 100644 index 0000000000..91336519b4 --- /dev/null +++ b/showcase/src/app/getting-started/getting-started.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { SharedModule } from "../shared"; +import { GettingStartedComponent } from "./getting-started-component"; +import { MatExpansionModule } from "@angular/material/expansion"; +import { StarkPrettyPrintModule } from "@nationalbankbelgium/stark-ui"; + +@NgModule({ + imports: [CommonModule, SharedModule, MatExpansionModule, StarkPrettyPrintModule], + declarations: [GettingStartedComponent], + exports: [GettingStartedComponent] +}) +export class GettingStartedModule {} diff --git a/showcase/src/app/getting-started/index.ts b/showcase/src/app/getting-started/index.ts new file mode 100644 index 0000000000..30be120517 --- /dev/null +++ b/showcase/src/app/getting-started/index.ts @@ -0,0 +1,2 @@ +export * from "./getting-started-component"; +export * from "./getting-started.module"; diff --git a/showcase/src/app/shared/example-viewer/_example-viewer-theme.scss b/showcase/src/app/shared/example-viewer/_example-viewer-theme.scss index 42b23a8f24..bf898f15ab 100644 --- a/showcase/src/app/shared/example-viewer/_example-viewer-theme.scss +++ b/showcase/src/app/shared/example-viewer/_example-viewer-theme.scss @@ -16,7 +16,7 @@ example-viewer { background-color: mat-color($mat-light-theme-background, card); } .mat-tab-body { - background-color: #272822; + background-color: $custom-dark-background; } .example-source { border-bottom: 1px solid mat-color($mat-light-theme-foreground, divider); diff --git a/showcase/src/app/shared/index.ts b/showcase/src/app/shared/index.ts index dbaeeb77f0..f2addf9021 100644 --- a/showcase/src/app/shared/index.ts +++ b/showcase/src/app/shared/index.ts @@ -1,3 +1,4 @@ export * from "./example-viewer"; export * from "./reference-block"; export * from "./shared.module"; +export * from "./table-of-contents"; diff --git a/showcase/src/app/shared/shared.module.ts b/showcase/src/app/shared/shared.module.ts index 4f6f09f4b6..65a0876b6d 100644 --- a/showcase/src/app/shared/shared.module.ts +++ b/showcase/src/app/shared/shared.module.ts @@ -12,6 +12,7 @@ import { NgModule } from "@angular/core"; import { StarkPrettyPrintModule } from "@nationalbankbelgium/stark-ui"; import { TranslateModule } from "@ngx-translate/core"; import { FlexLayoutModule } from "@angular/flex-layout"; +import { TableOfContentsComponent } from "./table-of-contents/table-of-contents.component"; @NgModule({ imports: [ @@ -27,8 +28,8 @@ import { FlexLayoutModule } from "@angular/flex-layout"; TranslateModule ], providers: [FileService], - declarations: [ExampleViewerComponent, ReferenceBlockComponent], - entryComponents: [ExampleViewerComponent, ReferenceBlockComponent], - exports: [ExampleViewerComponent, ReferenceBlockComponent, FlexLayoutModule] + declarations: [ExampleViewerComponent, ReferenceBlockComponent, TableOfContentsComponent], + entryComponents: [ExampleViewerComponent, ReferenceBlockComponent, TableOfContentsComponent], + exports: [ExampleViewerComponent, ReferenceBlockComponent, TableOfContentsComponent, FlexLayoutModule] }) export class SharedModule {} diff --git a/showcase/src/app/shared/table-of-contents/_table-of-contents-theme.scss b/showcase/src/app/shared/table-of-contents/_table-of-contents-theme.scss new file mode 100644 index 0000000000..8ee344f60a --- /dev/null +++ b/showcase/src/app/shared/table-of-contents/_table-of-contents-theme.scss @@ -0,0 +1,7 @@ +.table-of-contents-container { + border-left: solid 4px mat-color($primary-palette, 800); +} + +.table-of-contents-active-link { + color: mat-color($grey-palette, 900); +} diff --git a/showcase/src/app/shared/table-of-contents/index.ts b/showcase/src/app/shared/table-of-contents/index.ts new file mode 100644 index 0000000000..d15d08ee1b --- /dev/null +++ b/showcase/src/app/shared/table-of-contents/index.ts @@ -0,0 +1,2 @@ +export * from "./table-of-contents.component"; +export * from "./table-of-content-link.intf"; diff --git a/showcase/src/app/shared/table-of-contents/table-of-content-link.intf.ts b/showcase/src/app/shared/table-of-contents/table-of-content-link.intf.ts new file mode 100644 index 0000000000..c5474879d2 --- /dev/null +++ b/showcase/src/app/shared/table-of-contents/table-of-content-link.intf.ts @@ -0,0 +1,29 @@ +/** + * Defines the link to display in the table of contents + */ +export interface TableOfContentLink { + /** + * id of the section + */ + id: string; + + /** + * header type h1/h2 + */ + type: string; + + /** + * If the anchor is in view of the page + */ + active: boolean; + + /** + * name of the anchor + */ + name: string; + + /** + * top offset px of the anchor + */ + top: number; +} diff --git a/showcase/src/app/shared/table-of-contents/table-of-contents.component.html b/showcase/src/app/shared/table-of-contents/table-of-contents.component.html new file mode 100644 index 0000000000..f9324f23f8 --- /dev/null +++ b/showcase/src/app/shared/table-of-contents/table-of-contents.component.html @@ -0,0 +1,11 @@ +
+
Contents
+ +
diff --git a/showcase/src/app/shared/table-of-contents/table-of-contents.component.scss b/showcase/src/app/shared/table-of-contents/table-of-contents.component.scss new file mode 100644 index 0000000000..bd161086a3 --- /dev/null +++ b/showcase/src/app/shared/table-of-contents/table-of-contents.component.scss @@ -0,0 +1,59 @@ +:host { + font-size: 13px; + // Width is container width minus content width + width: 19%; + position: sticky; + top: 0; + padding-left: 25px; + box-sizing: border-box; + display: inline-flex; +} + +.table-of-contents-heading { + margin: 8px 5px 0; + padding: 0; + font-size: 13px; + font-weight: bold; +} + +.table-of-contents-link { + line-height: 20px; + margin: 8px 8px 0; + position: relative; + text-decoration: none; + display: block; + width: 100%; + overflow: hidden; + cursor: pointer; +} + +@media #{$desktop-lg-query} { + .table-of-contents-container { + padding: 5px 0 10px 10px; + width: 200px; + position: fixed; + top: 145px; + transform: translateX(375%); + } +} + +@media #{$handset-toc-query-screen}{ + .table-of-contents-container { + padding: 5px 0 10px 10px; + width: 500px; + left: 40px; + position: inherit; + display: inline-table;; + } +} + +@media #{$tablet-toc-query-screen} { + .table-of-contents-container { + padding: 5px 0 10px 10px; + position: fixed; + top: 145px; + width: 200px; + display: inline-table; + right: 0; + } +} diff --git a/showcase/src/app/shared/table-of-contents/table-of-contents.component.spec.ts b/showcase/src/app/shared/table-of-contents/table-of-contents.component.spec.ts new file mode 100644 index 0000000000..6ca88f0715 --- /dev/null +++ b/showcase/src/app/shared/table-of-contents/table-of-contents.component.spec.ts @@ -0,0 +1,84 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { TableOfContentsComponent } from "./table-of-contents.component"; +import { TableOfContentLink } from "./table-of-content-link.intf"; + +describe("TableOfContents", () => { + let fixture: ComponentFixture; + let component: TableOfContentsComponent; + + beforeEach(async(() => { + return TestBed.configureTestingModule({ + declarations: [TableOfContentsComponent], + imports: [] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TableOfContentsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should have no header", () => { + const header: Element = fixture.nativeElement.querySelector("h2"); + expect(header).toBeNull(); + }); + + it("should scroll to the right position", () => { + component.links = [ + { + type: "h2", + id: "test", + name: "test", + top: 52, + active: false + } + ]; + + fixture.detectChanges(); + const myAnchor: string = component.links[0].id; + spyOn(document, "querySelector").and.returnValue(document); + + component.scrollToAnchor(myAnchor); + const pos: number = (document.documentElement.scrollTop || document.body.scrollTop) + document.documentElement.offsetHeight; + expect(pos).toBe(52); + }); + + it("should have header and links", () => { + component.links = [ + { + type: "h2", + id: "test", + name: "test", + top: 0, + active: false + } + ]; + + const header: Element = fixture.nativeElement.querySelector("h2"); + expect(header).toBeDefined(); + + const links: Element = fixture.nativeElement.querySelector("li"); + expect(links).toBeDefined(); + }); + + it("should have created links", () => { + let links: TableOfContentLink[]; + fixture.detectChanges(); + const headerSelector: Partial[] = [ + { innerText: "first link", offsetTop: 25, tagName: "FIRST", id: "1" }, + { innerText: "second link", offsetTop: 50, tagName: "SECOND", id: "2" } + ]; + + spyOn(document, "querySelectorAll").and.returnValue(headerSelector); + + const expectedFirstLink: TableOfContentLink = { name: "first link", type: "first", top: 5, id: "1", active: false }; + const expectedSecondLink: TableOfContentLink = { name: "second link", type: "second", top: 30, id: "2", active: false }; + + links = component.createLinks(); + expect(links).not.toBeNull(); + expect(links.length).toBe(2); + expect(links[0]).toEqual(expectedFirstLink); + expect(links[1]).toEqual(expectedSecondLink); + }); +}); diff --git a/showcase/src/app/shared/table-of-contents/table-of-contents.component.ts b/showcase/src/app/shared/table-of-contents/table-of-contents.component.ts new file mode 100644 index 0000000000..daf4528024 --- /dev/null +++ b/showcase/src/app/shared/table-of-contents/table-of-contents.component.ts @@ -0,0 +1,164 @@ +import { AfterViewInit, Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { fromEvent, Subject } from "rxjs"; +import { debounceTime, takeUntil } from "rxjs/operators"; +import { StarkAction } from "@nationalbankbelgium/stark-ui"; +import { TableOfContentLink } from "./table-of-content-link.intf"; + +@Component({ + selector: "table-of-contents", + templateUrl: "./table-of-contents.component.html" +}) +/** + * The table of contents component, freely inspired from : + * @link https://github.com/angular/material.angular.io/tree/master/src/app/shared/table-of-contents + */ +export class TableOfContentsComponent implements OnInit, AfterViewInit, OnDestroy { + public links: TableOfContentLink[] = []; + + /** + * The classes of the titles you want to use in your table of contents + */ + @Input() + public headerSelectors: string; + + private _scrollContainer: Window; + private _destroyed: Subject = new Subject(); + private _urlFragment: string = ""; + + public constructor() { + // + } + + public ngOnInit(): void { + this.links = this.createLinks(); + + // On init, the sidenav content element doesn't yet exist, so it's not possible + // to subscribe to its scroll event until next tick (when it does exist). + Promise.resolve() + .then(() => { + this._scrollContainer = window; + if (this._scrollContainer) { + fromEvent(this._scrollContainer, "scroll") + .pipe( + takeUntil(this._destroyed), + debounceTime(10) + ) + .subscribe(() => { + this.onScroll(); + }); + } + }) + .catch(); + } + + public ngAfterViewInit(): void { + this.updateScrollPosition(); + } + + public ngOnDestroy(): void { + this._destroyed.next(); + } + + public updateScrollPosition(): void { + this.links = this.createLinks(); + const target: HTMLElement | null = document.getElementById(this._urlFragment); + if (target) { + target.scrollIntoView(); + } + } + + /** + * Gets the scrolloffset of the container. + * Basically, this will return the position on the screen where the user is located. + */ + private getScrollOffset(): number { + let top: number = 0; + if (typeof this._scrollContainer.pageYOffset !== "undefined") { + top = top + this._scrollContainer.pageYOffset; + } + return top; + } + + /** + * This method will build a list of links according to the classes the user put in headerSelectors. + * @returns a list of links + */ + public createLinks(): TableOfContentLink[] { + const links: TableOfContentLink[] = []; + const headings: HTMLElement[] = Array.from(document.querySelectorAll(this.headerSelectors)); + + for (const heading of headings) { + // remove the 'link' icon name from the inner text + const name: string = heading.innerText.trim().replace(/^link/, ""); + const top: number = heading.offsetTop - 20; + links.push({ + name, + type: heading.tagName.toLowerCase(), + top: top, + id: heading.id, + active: false + }); + } + return links; + } + + /** + * This methods checks if each link is active or not + * we have to do a trick here for the last link of the page. + * We calculate the position on the screen as well as the lowest position of the page. + * If they match, it means that we are at the bottom of the page and that the last link of the table should be active, and the one + * before it should therefore not. + * + * When we scroll again on the page, the last link is set to inactive again. + * This is used when the offset position is lower than the top of the last link on the page (if that so, that link could never + * be set to active). + */ + private onScroll(): void { + const linksCounter: number = this.links.length - 1; + const max: number = document.documentElement.scrollHeight; + const pos: number = (document.documentElement.scrollTop || document.body.scrollTop) + document.documentElement.offsetHeight; + if (pos === max) { + this.links[linksCounter].active = true; + for (let i: number = 0; i < linksCounter; i++) { + this.links[i].active = false; + } + } else { + this.links[linksCounter].active = false; + for (let i: number = 0; i < linksCounter; i++) { + this.links[i].active = this.isLinkActive(this.links[i], this.links[i + 1]); + } + } + } + + /** + * This method will check, thanks to the user's position on a screen, if a link is active or not. + * If the value of scrolloffset is higher than the top of the currentlink (so, below the currentLink) + * and lower than the top of tne nextlinkg (so, above the nextLink), it means that the link is active. + * @param currentLink - the Link we are analysing + * @param nextLink - the Link that follows the currentLink + * @returns - if the link is active (true) or not (false) + */ + private isLinkActive(currentLink: TableOfContentLink, nextLink: TableOfContentLink): boolean { + // A link is considered active if the page is scrolled passed the anchor without also + // being scrolled passed the next link + const scrollOffset: number = this.getScrollOffset(); + return scrollOffset >= currentLink.top && (!nextLink || nextLink.top > scrollOffset); + } + + /** + * Method used to scroll to a link directly. + * We cannot use the classical href with id's method because it would logoff the user from the application. + * @param anchor - the title to scroll to + */ + public scrollToAnchor(anchor: string): void { + const anchorPosition: number = (document.querySelector(anchor)).offsetTop; + window.scrollTo(0, anchorPosition); + } + + /** + * @ignore + */ + public trackItem(_index: number, item: StarkAction): string { + return item.id; + } +} diff --git a/showcase/src/styles/_theme.scss b/showcase/src/styles/_theme.scss index 6afc833902..46c7a958c5 100644 --- a/showcase/src/styles/_theme.scss +++ b/showcase/src/styles/_theme.scss @@ -8,3 +8,5 @@ @import "../app/shared/example-viewer/example-viewer-theme"; @import "../app/app.component-theme"; +@import "../app/shared/table-of-contents/table-of-contents-theme"; +@import "../app/getting-started/getting-started-component/getting-started-theme"; diff --git a/showcase/src/styles/styles.scss b/showcase/src/styles/styles.scss index a3eca48c6a..8c90715876 100644 --- a/showcase/src/styles/styles.scss +++ b/showcase/src/styles/styles.scss @@ -6,6 +6,8 @@ @import "stark-styles.scss"; @import "../app/app.component"; @import "../app/home/home.component"; +@import "../app/getting-started/getting-started-component/getting-started.component"; @import "../app/news/news-component/news.component"; @import "../app/news/news-item-component/news-item.component"; @import "../app/shared/reference-block/reference-block.component"; +@import "../app/shared/table-of-contents/table-of-contents.component";