From 3e1a0c771d61447b7fee911304ccae0bf9b1a37a Mon Sep 17 00:00:00 2001 From: josee666 Date: Wed, 12 Aug 2020 10:03:42 -0400 Subject: [PATCH] feat(interactiveTour + welcomeWindows): add new component and service (#701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * tour * lint * feat add noBackButton on step Co-authored-by: Marc-André Barbeau Co-authored-by: Josée Martel --- angular.json | 1 + demo/src/app/core/home/home.component.html | 16 +- demo/src/app/core/home/home.component.ts | 9 +- demo/src/app/core/home/home.module.ts | 12 +- demo/src/config/interactiveTour.json | 50 +++ demo/src/environments/environment.prod.ts | 3 + demo/src/environments/environment.ts | 27 +- demo/src/locale/en.json | 10 +- demo/src/locale/fr.json | 10 +- package-lock.json | 32 +- package.json | 1 + .../lib/auth-form/auth-form.component.html | 6 +- .../src/lib/auth-form/auth-form.component.ts | 11 +- packages/auth/src/lib/auth.module.ts | 2 +- packages/auth/src/lib/shared/auth.service.ts | 11 +- .../common/src/lib/interactive-tour/index.ts | 4 + .../interactive-tour.component.html | 10 + .../interactive-tour.component.scss | 8 + .../interactive-tour.component.ts | 101 ++++++ .../interactive-tour.interface.ts | 37 +++ .../interactive-tour.loader.ts | 44 +++ .../interactive-tour.module.ts | 24 ++ .../interactive-tour.service.ts | 292 ++++++++++++++++++ .../interactive-tour.theming.scss | 40 +++ .../common/src/lib/panel/panel.component.html | 2 +- .../common/src/lib/panel/panel.component.ts | 3 - packages/common/src/lib/panel/panel.module.ts | 12 +- .../src/lib/tool/shared/tool.service.ts | 12 +- packages/common/src/locale/en.common.json | 7 + packages/common/src/locale/fr.common.json | 7 + packages/common/src/public_api.ts | 2 + packages/common/src/style/common.theming.scss | 4 + .../about-tool/about-tool.component.html | 10 +- .../about-tool/about-tool.component.scss | 3 + .../about/about-tool/about-tool.component.ts | 3 +- .../integration/src/lib/about/about.module.ts | 14 +- .../context-editor-tool.component.html | 5 +- .../search/search-bar/search-bar.module.ts | 6 +- .../integration/src/lib/tool/tool.state.ts | 11 +- 39 files changed, 800 insertions(+), 62 deletions(-) create mode 100644 demo/src/config/interactiveTour.json create mode 100644 packages/common/src/lib/interactive-tour/index.ts create mode 100644 packages/common/src/lib/interactive-tour/interactive-tour.component.html create mode 100644 packages/common/src/lib/interactive-tour/interactive-tour.component.scss create mode 100644 packages/common/src/lib/interactive-tour/interactive-tour.component.ts create mode 100644 packages/common/src/lib/interactive-tour/interactive-tour.interface.ts create mode 100644 packages/common/src/lib/interactive-tour/interactive-tour.loader.ts create mode 100644 packages/common/src/lib/interactive-tour/interactive-tour.module.ts create mode 100644 packages/common/src/lib/interactive-tour/interactive-tour.service.ts create mode 100644 packages/common/src/lib/interactive-tour/interactive-tour.theming.scss create mode 100644 packages/integration/src/lib/about/about-tool/about-tool.component.scss diff --git a/angular.json b/angular.json index e03fb521f0..711ef1c400 100644 --- a/angular.json +++ b/angular.json @@ -27,6 +27,7 @@ "assets": [ "demo/src/favicon.ico", "demo/src/assets", + "demo/src/config", "demo/src/contexts", "demo/src/locale", { diff --git a/demo/src/app/core/home/home.component.html b/demo/src/app/core/home/home.component.html index 58c64db0d9..9817c23dd8 100644 --- a/demo/src/app/core/home/home.component.html +++ b/demo/src/app/core/home/home.component.html @@ -1,13 +1,21 @@
-

+

Welcome to IGO

- +

-IGO2 library contains many components and services that may benefit any web application.
-IGO2 library is open source project using Angular, Angular Material and OpenLayers. + + +

+ +

+ IGO2 library contains many components and services that may benefit any web application.
+ IGO2 library is open source project using Angular, Angular Material and OpenLayers.

IGO2 library is divided into several elements:

diff --git a/demo/src/app/core/home/home.component.ts b/demo/src/app/core/home/home.component.ts index 67e48ff2d0..9dd4f69e28 100644 --- a/demo/src/app/core/home/home.component.ts +++ b/demo/src/app/core/home/home.component.ts @@ -1,8 +1,15 @@ import { Component } from '@angular/core'; +import { InteractiveTourService } from '@igo2/common'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) -export class AppHomeComponent {} +export class AppHomeComponent { + constructor(private interactiveTourService: InteractiveTourService) {} + + startTour() { + this.interactiveTourService.startTour('global'); + } +} diff --git a/demo/src/app/core/home/home.module.ts b/demo/src/app/core/home/home.module.ts index 25102bc6d0..efcdc9ba84 100644 --- a/demo/src/app/core/home/home.module.ts +++ b/demo/src/app/core/home/home.module.ts @@ -1,11 +1,21 @@ import { NgModule } from '@angular/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; import { AppHomeComponent } from './home.component'; import { AppHomeRoutingModule } from './home-routing.module'; +import { IgoInteractiveTourModule } from '@igo2/common'; @NgModule({ declarations: [AppHomeComponent], - imports: [AppHomeRoutingModule], + imports: [ + AppHomeRoutingModule, + IgoInteractiveTourModule, + MatTooltipModule, + MatIconModule, + MatButtonModule + ], exports: [AppHomeComponent] }) export class AppHomeModule {} diff --git a/demo/src/config/interactiveTour.json b/demo/src/config/interactiveTour.json new file mode 100644 index 0000000000..afd4065f40 --- /dev/null +++ b/demo/src/config/interactiveTour.json @@ -0,0 +1,50 @@ +{ + "global": { + "title": "interactiveTour.title", + "highlightClass": "mat-form-field", + "class": "mat-form-field", + "steps": [ + { + "element": "img.igo-logo", + "text": "interactiveTour.selectByClass" + }, + { + "element": "#igo-title", + "text": "interactiveTour.selectById", + "noBackButton": true, + "beforeShow": { + "element": "a.mat-list-item[routerlink=\\.]", + "action": "click" + } + }, + { + "element": "a.mat-list-item[routerlink=context]", + "text": "interactiveTour.changeTool", + "onShow": { + "action": "click" + } + }, + { + "element": "igo-context-item:nth-of-type(3)", + "text": "interactiveTour.context3" + }, + { + "element": "igo-context-item:nth-of-type(3)", + "text": "Action 'onShow'", + "onShow": { + "action": "click" + } + }, + { + "element": "igo-layer-item:nth-of-type(2) button", + "text": "Action 'onHide'", + "onHide": { + "action": "click" + } + }, + { + "text": "interactiveTour.end" + } + ] + } +} diff --git a/demo/src/environments/environment.prod.ts b/demo/src/environments/environment.prod.ts index ed85631c26..aed19e1cb0 100644 --- a/demo/src/environments/environment.prod.ts +++ b/demo/src/environments/environment.prod.ts @@ -21,6 +21,9 @@ export const environment: Environment = { }, allowAnonymous: true }, + interactiveTour: { + tourInMobile: true + }, importExport: { url: 'https://geoegl.msp.gouv.qc.ca/apis/ogre' }, diff --git a/demo/src/environments/environment.ts b/demo/src/environments/environment.ts index 62524f2d65..c331eb8c84 100644 --- a/demo/src/environments/environment.ts +++ b/demo/src/environments/environment.ts @@ -33,6 +33,9 @@ export const environment: Environment = { language: { prefix: './locale/' }, + interactiveTour: { + tourInMobile: true + }, importExport: { url: '/apis/ogre', gpxAggregateInComment: true @@ -80,7 +83,8 @@ export const environment: Environment = { }, { id: 'rn_wmts', - url: 'https://servicesmatriciels.mern.gouv.qc.ca/erdas-iws/ogc/wmts/Cartes_Images', + url: + 'https://servicesmatriciels.mern.gouv.qc.ca/erdas-iws/ogc/wmts/Cartes_Images', type: 'wmts', crossOrigin: true, matrixSet: 'EPSG_3857', @@ -90,34 +94,39 @@ export const environment: Environment = { }, { id: 'group_impose', - title: '(composite catalog) group imposed and unique layer title for same source', + title: + '(composite catalog) group imposed and unique layer title for same source', composite: [ { id: 'tq_swtq', url: 'https://geoegl.msp.gouv.qc.ca/apis/ws/swtq', regFilters: ['zpegt'], - groupImpose: {id: 'zpegt', title: 'zpegt'} + groupImpose: { id: 'zpegt', title: 'zpegt' } }, { id: 'Gououvert', url: 'https://geoegl.msp.gouv.qc.ca/apis/ws/igo_gouvouvert.fcgi', regFilters: ['zpegt'], - groupImpose: {id: 'zpegt', title: 'zpegt'} + groupImpose: { id: 'zpegt', title: 'zpegt' } }, { id: 'Gououvert', url: 'https://geoegl.msp.gouv.qc.ca/apis/ws/igo_gouvouvert.fcgi', regFilters: ['zpegt'], - groupImpose: {id: 'zpegt', title: 'zpegt'} + groupImpose: { id: 'zpegt', title: 'zpegt' } }, { id: 'rn_wmts', - url: 'https://servicesmatriciels.mern.gouv.qc.ca/erdas-iws/ogc/wmts/Cartes_Images', + url: + 'https://servicesmatriciels.mern.gouv.qc.ca/erdas-iws/ogc/wmts/Cartes_Images', type: 'wmts', crossOrigin: true, matrixSet: 'EPSG_3857', version: '1.0.0', - groupImpose: {id: 'cartetopo', title: 'Carte topo échelle 1/20 000'} + groupImpose: { + id: 'cartetopo', + title: 'Carte topo échelle 1/20 000' + } } ] }, @@ -129,13 +138,13 @@ export const environment: Environment = { id: 'tq_swtq', url: 'https://geoegl.msp.gouv.qc.ca/apis/ws/swtq', regFilters: ['limtn_charg'], - groupImpose: {id: 'mix_swtq_gouv', title: 'mix same name layer'} + groupImpose: { id: 'mix_swtq_gouv', title: 'mix same name layer' } }, { id: 'Gououvert', url: 'https://geoegl.msp.gouv.qc.ca/apis/ws/igo_gouvouvert.fcgi', regFilters: ['limtn_charg'], - groupImpose: {id: 'mix_swtq_gouv', title: 'mix same name layer'} + groupImpose: { id: 'mix_swtq_gouv', title: 'mix same name layer' } } ] } diff --git a/demo/src/locale/en.json b/demo/src/locale/en.json index 7924656796..66e07a5b22 100644 --- a/demo/src/locale/en.json +++ b/demo/src/locale/en.json @@ -10,5 +10,13 @@ "Delete Tooltip": "Delete Tooltip", "Add": "Add", "Edit": "Edit", - "Delete": "Delete" + "Delete": "Delete", + "interactiveTour": { + "title": "Interactive Tour", + "selectByClass": "Sselect by class", + "selectById": "Select by id", + "changeTool": "Change tool", + "context3": "Selection of the 3rd context of the list with nth-of-type", + "end": "End of tour" + } } diff --git a/demo/src/locale/fr.json b/demo/src/locale/fr.json index 4c2a6a4b5d..b0d8cd9396 100644 --- a/demo/src/locale/fr.json +++ b/demo/src/locale/fr.json @@ -10,5 +10,13 @@ "Delete Tooltip": "Supprimer Tooltip", "Add": "Ajouter", "Edit": "Éditer", - "Delete": "Supprimer" + "Delete": "Supprimer", + "interactiveTour": { + "title": "Tour interactif", + "selectByClass": "Sélection par classe", + "selectById": "Sélection par id", + "changeTool": "Changement d'outil", + "context3": "Sélection du 3e contexte de la liste avec nth-of-type", + "end": "Fin du tour" + } } diff --git a/package-lock.json b/package-lock.json index 6144602456..103a5a5476 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2756,6 +2756,11 @@ } } }, + "@popperjs/core": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.4.4.tgz", + "integrity": "sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==" + }, "@rollup/plugin-commonjs": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-13.0.0.tgz", @@ -3762,6 +3767,15 @@ } } }, + "angular-shepherd": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/angular-shepherd/-/angular-shepherd-0.7.0.tgz", + "integrity": "sha512-pM9ibVJumxQ9tIzkhnr1KpbJJKLaXpofotxn5H/3tVkO5RtijSQ3Y3fZDF864R4SXUnW/wND1pmnRaS5KUpTdQ==", + "requires": { + "shepherd.js": "^8.0.1", + "tslib": "^2.0.0" + } + }, "angular2-notifications": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/angular2-notifications/-/angular2-notifications-9.0.0.tgz", @@ -7008,8 +7022,7 @@ "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, "default-compare": { "version": "1.0.0", @@ -17312,6 +17325,16 @@ "rechoir": "^0.6.2" } }, + "shepherd.js": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/shepherd.js/-/shepherd.js-8.0.2.tgz", + "integrity": "sha512-qFt5vrnVoshg6fS5gKGQznYGyIaEclyKKSl1Siw6gO7GyKnCEJ2aBDrp8pxKusMfZo5J5dtgeKJtsRav5cimjA==", + "requires": { + "@popperjs/core": "^2.4.4", + "deepmerge": "^4.2.2", + "smoothscroll-polyfill": "^0.4.4" + } + }, "sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", @@ -17359,6 +17382,11 @@ "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", "dev": true }, + "smoothscroll-polyfill": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz", + "integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==" + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", diff --git a/package.json b/package.json index a0ffe3c73e..0735ddb08d 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@turf/helpers": "^6.1.4", "@turf/line-intersect": "^6.0.2", "@turf/point-on-feature": "^5.1.5", + "angular-shepherd": "^0.7.0", "angular2-notifications": "^9.0.0", "bowser": "^2.10.0", "classlist.js": "^1.1.20150312", diff --git a/packages/auth/src/lib/auth-form/auth-form.component.html b/packages/auth/src/lib/auth-form/auth-form.component.html index 520a77d410..978cb2e08e 100644 --- a/packages/auth/src/lib/auth-form/auth-form.component.html +++ b/packages/auth/src/lib/auth-form/auth-form.component.html @@ -6,16 +6,16 @@

{{'igo.auth.connection' | translate}}

+ (login)="onLogin()"> + (login)="onLogin()"> + (login)="onLogin()"> diff --git a/packages/auth/src/lib/auth-form/auth-form.component.ts b/packages/auth/src/lib/auth-form/auth-form.component.ts index c8f051d453..b6951643eb 100644 --- a/packages/auth/src/lib/auth-form/auth-form.component.ts +++ b/packages/auth/src/lib/auth-form/auth-form.component.ts @@ -3,7 +3,9 @@ import { ChangeDetectionStrategy, OnInit, Input, - Optional + Optional, + Output, + EventEmitter } from '@angular/core'; import { Router, NavigationStart } from '@angular/router'; import { filter } from 'rxjs/operators'; @@ -79,6 +81,8 @@ export class AuthFormComponent implements OnInit { } } + @Output() login: EventEmitter = new EventEmitter(); + public options: AuthOptions; public user; @@ -101,9 +105,10 @@ export class AuthFormComponent implements OnInit { this.getName(); } - public login() { + public onLogin() { this.auth.goToRedirectUrl(); this.getName(); + this.login.emit(true); } public logout() { @@ -140,7 +145,7 @@ export class AuthFormComponent implements OnInit { } this.router.events - .pipe(filter(event => event instanceof NavigationStart)) + .pipe(filter((event) => event instanceof NavigationStart)) .subscribe((changeEvent: any) => { if (changeEvent.url) { const currentRoute = changeEvent.url; diff --git a/packages/auth/src/lib/auth.module.ts b/packages/auth/src/lib/auth.module.ts index ec2946acda..07763bfe12 100644 --- a/packages/auth/src/lib/auth.module.ts +++ b/packages/auth/src/lib/auth.module.ts @@ -6,7 +6,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { IgoLanguageModule, StorageService } from '@igo2/core'; +import { StorageService, IgoLanguageModule } from '@igo2/core'; import { AuthStorageService } from './shared/storage.service'; import { ProtectedDirective } from './shared/protected.directive'; diff --git a/packages/auth/src/lib/shared/auth.service.ts b/packages/auth/src/lib/shared/auth.service.ts index 2872954832..bada453a84 100644 --- a/packages/auth/src/lib/shared/auth.service.ts +++ b/packages/auth/src/lib/shared/auth.service.ts @@ -17,6 +17,7 @@ import { TokenService } from './token.service'; }) export class AuthService { public authenticate$ = new BehaviorSubject(undefined); + public logged$ = new BehaviorSubject(undefined); public redirectUrl: string; private anonymous = false; @@ -29,7 +30,8 @@ export class AuthService { @Optional() private router: Router ) { this.authenticate$.next(this.authenticated); - this.authenticate$.subscribe(() => { + this.authenticate$.subscribe((authenticated) => { + this.logged$.next(authenticated); globalCacheBusterNotifier.next(); }); } @@ -60,6 +62,7 @@ export class AuthService { loginAnonymous(): Observable { this.anonymous = true; + this.logged$.next(true); return of(true); } @@ -69,7 +72,7 @@ export class AuthService { tap((data: any) => { this.tokenService.set(data.token); }), - catchError(err => { + catchError((err) => { err.error.caught = true; throw err; }) @@ -166,14 +169,14 @@ export class AuthService { if (tokenDecoded.user.isExpired) { this.languageService.translate .get('igo.auth.error.Password expired') - .subscribe(expiredAlert => + .subscribe((expiredAlert) => this.messageService.alert(expiredAlert) ); } } this.authenticate$.next(true); }), - catchError(err => { + catchError((err) => { err.error.caught = true; throw err; }) diff --git a/packages/common/src/lib/interactive-tour/index.ts b/packages/common/src/lib/interactive-tour/index.ts new file mode 100644 index 0000000000..f9d40dbbe1 --- /dev/null +++ b/packages/common/src/lib/interactive-tour/index.ts @@ -0,0 +1,4 @@ +export * from './interactive-tour.service'; +export * from './interactive-tour.component'; +export * from './interactive-tour.loader'; +export * from './interactive-tour.interface'; diff --git a/packages/common/src/lib/interactive-tour/interactive-tour.component.html b/packages/common/src/lib/interactive-tour/interactive-tour.component.html new file mode 100644 index 0000000000..314a8f6f76 --- /dev/null +++ b/packages/common/src/lib/interactive-tour/interactive-tour.component.html @@ -0,0 +1,10 @@ + diff --git a/packages/common/src/lib/interactive-tour/interactive-tour.component.scss b/packages/common/src/lib/interactive-tour/interactive-tour.component.scss new file mode 100644 index 0000000000..7e0436c993 --- /dev/null +++ b/packages/common/src/lib/interactive-tour/interactive-tour.component.scss @@ -0,0 +1,8 @@ +.shepherd-has-title .shepherd-content .shepherd-header { + padding: 0.5em 0.75em; +} + +.shepherd-progress { + margin-right: 15px; + color: hsla(0, 0%, 45%, 1); +} diff --git a/packages/common/src/lib/interactive-tour/interactive-tour.component.ts b/packages/common/src/lib/interactive-tour/interactive-tour.component.ts new file mode 100644 index 0000000000..13ccbb7842 --- /dev/null +++ b/packages/common/src/lib/interactive-tour/interactive-tour.component.ts @@ -0,0 +1,101 @@ +import { Component, ViewEncapsulation, Input } from '@angular/core'; +import { InteractiveTourService } from './interactive-tour.service'; +import { ToolService } from '../tool/shared/tool.service'; + +@Component({ + selector: 'igo-interactive-tour', + templateUrl: './interactive-tour.component.html', + styleUrls: ['./interactive-tour.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class InteractiveTourComponent { + /** + * Toolbox that holds main tools + */ + @Input() tourToStart: string = ''; + @Input() styleButton: string; + + getClass() { + return { + 'tour-button-tool-icon': this.styleButton === 'icon', + 'tour-button-tool': this.styleButton === 'raised' + }; + } + + get toolbox() { + return this.toolService.toolbox; + } + + getTourToStart() { + if (this.tourToStart) { + return this.tourToStart; + } else { + return this.activeToolName; + } + } + + get activeToolName() { + if (this.toolbox) { + if (this.isActiveTool) { + return this.toolbox.activeTool$.getValue().name; + } else { + return 'global'; + } + } else { + return undefined; + } + } + + get isActiveTool(): boolean { + if (this.toolbox) { + return this.toolbox.activeTool$.getValue() !== undefined; + } else { + return undefined; + } + } + + get isToolHaveTour(): boolean { + if (this.activeToolName === 'about' && !this.tourToStart) { + return false; + } + return this.interactiveTourService.isToolHaveTourConfig( + this.getTourToStart() + ); + } + + get showTourButton(): boolean { + // 2 conditions to show: have Tour on tool in Config file and if we are in mobile displayInMobile= true + let haveTour: boolean; + haveTour = this.isToolHaveTour; + if (haveTour === false) { + return false; + } + + let inMobileAndShow: boolean; + if (this.interactiveTourService.isMobile()) { + inMobileAndShow = this.isTourDisplayInMobile; + if (inMobileAndShow === false) { + return false; + } + } + return true; + } + + get isTourDisplayInMobile(): boolean { + return this.interactiveTourService.isTourDisplayInMobile(); + } + + constructor( + private interactiveTourService: InteractiveTourService, + private toolService: ToolService + ) {} + + startInteractiveTour() { + const tour = this.getTourToStart(); + if (tour) { + this.interactiveTourService.startTour(tour); + } else { + return; + } + } +} diff --git a/packages/common/src/lib/interactive-tour/interactive-tour.interface.ts b/packages/common/src/lib/interactive-tour/interactive-tour.interface.ts new file mode 100644 index 0000000000..0e80e898a1 --- /dev/null +++ b/packages/common/src/lib/interactive-tour/interactive-tour.interface.ts @@ -0,0 +1,37 @@ +export interface InteractiveTourStep { + element?: string; + position?: string; + title?: string; + text: string; + beforeShow?: InteractiveTourAction; + beforeChange?: InteractiveTourAction; + onShow?: InteractiveTourAction; + onHide?: InteractiveTourAction; + class?: string; + highlightClass?: string; + scrollToElement?: boolean; + disableInteraction?: boolean; + noBackButton?: boolean; +} + +export interface InteractiveTourAction { + element?: string; + action: 'click'; + condition?: string; + waitFor?: string; + maxWait?: number; // in millisecond +} + +export interface InteractiveTourOptions { + steps: InteractiveTourStep[]; + position?: string; + title?: string; + /* CSS class that is added to the hightlight element */ + highlightClass?: string; + /* CSS class that is added to the modal container */ + class?: string; + /* Scroll to highlighted element? */ + scrollToElement?: boolean; + /* Disable an interaction with element? */ + disableInteraction?: boolean; +} diff --git a/packages/common/src/lib/interactive-tour/interactive-tour.loader.ts b/packages/common/src/lib/interactive-tour/interactive-tour.loader.ts new file mode 100644 index 0000000000..421c025bb6 --- /dev/null +++ b/packages/common/src/lib/interactive-tour/interactive-tour.loader.ts @@ -0,0 +1,44 @@ +import { catchError } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { InteractiveTourOptions } from './interactive-tour.interface'; +import { ConfigService } from '@igo2/core'; + +@Injectable() +export class InteractiveTourLoader { + private jsonURL: string; + private allToursOptions; + + constructor(private http: HttpClient, private configService: ConfigService) { + this.jsonURL = this.getPathToConfigFile(); + this.allToursOptions = this.getJSON().subscribe((data) => { + this.allToursOptions = data; + }); + } + + public getPathToConfigFile(): string { + return ( + this.configService.getConfig('interactiveTour.pathToConfigFile') || + './config/interactiveTour.json' + ); + } + + public getJSON(): Observable { + return this.http.get(this.jsonURL).pipe( + catchError((e) => { + e.error.caught = true; + throw e; + }) + ); + } + + public getTourOptionData(toolName): InteractiveTourOptions { + if (this.allToursOptions === undefined) { + return undefined; + } + let nameInConfigFile = toolName; + nameInConfigFile = nameInConfigFile.replace(/\s/g, ''); + return this.allToursOptions[nameInConfigFile]; + } +} diff --git a/packages/common/src/lib/interactive-tour/interactive-tour.module.ts b/packages/common/src/lib/interactive-tour/interactive-tour.module.ts new file mode 100644 index 0000000000..6581f917ae --- /dev/null +++ b/packages/common/src/lib/interactive-tour/interactive-tour.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { CommonModule } from '@angular/common'; + +import { IgoLanguageModule } from '@igo2/core'; +import { InteractiveTourService } from './interactive-tour.service'; +import { InteractiveTourComponent } from './interactive-tour.component'; +import { InteractiveTourLoader } from './interactive-tour.loader'; + +@NgModule({ + declarations: [InteractiveTourComponent], + imports: [ + CommonModule, + MatIconModule, + MatButtonModule, + MatTooltipModule, + IgoLanguageModule + ], + providers: [InteractiveTourService, InteractiveTourLoader], + exports: [InteractiveTourComponent] +}) +export class IgoInteractiveTourModule {} diff --git a/packages/common/src/lib/interactive-tour/interactive-tour.service.ts b/packages/common/src/lib/interactive-tour/interactive-tour.service.ts new file mode 100644 index 0000000000..7cfc1e6656 --- /dev/null +++ b/packages/common/src/lib/interactive-tour/interactive-tour.service.ts @@ -0,0 +1,292 @@ +import { Injectable } from '@angular/core'; +import { ShepherdService } from 'angular-shepherd'; + +import { ConfigService, MediaService, LanguageService } from '@igo2/core'; +import { InteractiveTourLoader } from './interactive-tour.loader'; +import { + InteractiveTourOptions, + InteractiveTourStep, + InteractiveTourAction +} from './interactive-tour.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class InteractiveTourService { + private previousStep: InteractiveTourStep; + + constructor( + private configService: ConfigService, + private mediaService: MediaService, + private languageService: LanguageService, + private interactiveTourLoader: InteractiveTourLoader, + private shepherdService: ShepherdService + ) {} + + public isToolHaveTourConfig(toolName: string): boolean { + const checkTourActiveOptions = this.interactiveTourLoader.getTourOptionData( + toolName + ); + if (checkTourActiveOptions === undefined) { + return false; + } else { + return true; + } + } + + public isMobile(): boolean { + const media = this.mediaService.getMedia(); + if (media === 'mobile') { + return true; + } else { + return false; + } + } + + public isTourDisplayInMobile(): boolean { + const showInMobile = this.configService.getConfig( + 'interactiveTour.tourInMobile' + ); + if (showInMobile === undefined) { + return true; + } + return this.configService.getConfig('interactiveTour.tourInMobile'); + } + + private getButtons(buttonKind?: 'first' | 'last' | 'noBackButton') { + if (buttonKind === 'noBackButton') { + return [ + { + classes: 'shepherd-button-primary', + text: this.languageService.translate.instant( + 'igo.common.interactiveTour.nextButton' + ), + type: 'next' + } + ]; + } + if (buttonKind === 'first') { + return [ + { + classes: 'shepherd-button-secondary', + text: this.languageService.translate.instant( + 'igo.common.interactiveTour.exitButton' + ), + type: 'cancel' + }, + { + classes: 'shepherd-button-primary', + text: this.languageService.translate.instant( + 'igo.common.interactiveTour.nextButton' + ), + type: 'next' + } + ]; + } + + if (buttonKind === 'last') { + return [ + { + classes: 'shepherd-button-secondary', + text: this.languageService.translate.instant( + 'igo.common.interactiveTour.backButton' + ), + type: 'back' + }, + { + classes: 'shepherd-button-primary', + text: this.languageService.translate.instant( + 'igo.common.interactiveTour.exitButton' + ), + type: 'cancel' + } + ]; + } + + return [ + { + classes: 'shepherd-button-secondary', + text: this.languageService.translate.instant( + 'igo.common.interactiveTour.backButton' + ), + type: 'back' + }, + { + classes: 'shepherd-button-primary', + text: this.languageService.translate.instant( + 'igo.common.interactiveTour.nextButton' + ), + type: 'next' + } + ]; + } + + private getAction(actionName: string) { + const action = { + click: 'click' + }; + return action[actionName.toLowerCase()]; + } + + private addProgress() { + const self = this as any; + let nbTry = 0; + const maxTry = 21; + const checkExist = setInterval(() => { + if (self.getCurrentStep()) { + const currentStepElement = self.getCurrentStep().getElement(); + const header = currentStepElement + ? currentStepElement.querySelector('.shepherd-header') + : undefined; + + nbTry++; + if (header || nbTry > maxTry) { + clearInterval(checkExist); + } + + if (header) { + const stepsArray = self.steps; + const progress = document.createElement('span'); + progress.className = 'shepherd-progress'; + progress.innerText = `${ + stepsArray.indexOf(self.getCurrentStep()) + 1 + }/${stepsArray.length}`; + header.insertBefore( + progress, + currentStepElement.querySelector('.shepherd-cancel-icon') + ); + } + } + }, 100); + } + + private executeAction( + step: InteractiveTourStep, + actionConfig: InteractiveTourAction + ) { + if (!actionConfig) { + return; + } + + if ( + actionConfig.condition && + ((actionConfig.condition.charAt(0) === '!' && + document.querySelector(actionConfig.condition.slice(1))) || + (actionConfig.condition.charAt(0) !== '!' && + !document.querySelector(actionConfig.condition))) + ) { + return; + } + + const element: HTMLElement = document.querySelector( + actionConfig.element || step.element + ) as HTMLElement; + const action = this.getAction(actionConfig.action); + if (element && action) { + element[action](); + } + } + + private executeActionPromise( + step: InteractiveTourStep, + actionConfig: InteractiveTourAction + ) { + return new Promise((resolve) => { + this.executeAction(step, actionConfig); + if (!actionConfig || !actionConfig.waitFor) { + resolve(); + return; + } + let nbTry = 0; + const maxTry = actionConfig.maxWait ? actionConfig.maxWait / 100 : 20; + const checkExist = setInterval(() => { + nbTry++; + if (nbTry > maxTry || document.querySelector(actionConfig.waitFor)) { + clearInterval(checkExist); + resolve(); + } + }, 100); + }); + } + + private getShepherdSteps(stepConfig: InteractiveTourOptions) { + const shepherdSteps = []; + + let i = 0; + for (const step of stepConfig.steps) { + shepherdSteps.push({ + attachTo: { + element: step.element, + on: step.position || stepConfig.position + }, + beforeShowPromise: () => { + return Promise.all([ + this.executeActionPromise( + this.previousStep, + this.previousStep ? this.previousStep.beforeChange : undefined + ), + this.executeActionPromise(step, step.beforeShow) + ]); + }, + buttons: this.getButtons( + i === 0 + ? 'first' + : i + 1 === stepConfig.steps.length + ? 'last' + : stepConfig.steps[i].noBackButton + ? 'noBackButton' + : undefined + ), + classes: step.class, + highlightClass: step.highlightClass, + scrollTo: step.scrollToElement || stepConfig.scrollToElement || true, + canClickTarget: step.disableInteraction + ? !step.disableInteraction + : undefined, + title: this.languageService.translate.instant( + step.title || stepConfig.title + ), + text: [this.languageService.translate.instant(step.text)], + when: { + show: () => { + this.executeAction(step, step.onShow); + }, + hide: () => { + this.previousStep = step; + this.executeAction(step, step.onHide); + } + } + }); + i++; + } + + return shepherdSteps; + } + + public startTour(toolName: string) { + const stepConfig: InteractiveTourOptions = this.interactiveTourLoader.getTourOptionData( + toolName + ); + + this.shepherdService.defaultStepOptions = { + classes: stepConfig.class, + highlightClass: stepConfig.highlightClass, + canClickTarget: stepConfig.disableInteraction + ? !stepConfig.disableInteraction + : true, + cancelIcon: { + enabled: true + } + }; + + const shepherdSteps = this.getShepherdSteps(stepConfig); + + this.shepherdService.modal = true; + this.shepherdService.confirmCancel = false; + this.shepherdService.addSteps(shepherdSteps); + + this.shepherdService.tourObject.on('show', this.addProgress); + + this.shepherdService.start(); + } +} diff --git a/packages/common/src/lib/interactive-tour/interactive-tour.theming.scss b/packages/common/src/lib/interactive-tour/interactive-tour.theming.scss new file mode 100644 index 0000000000..94b6202093 --- /dev/null +++ b/packages/common/src/lib/interactive-tour/interactive-tour.theming.scss @@ -0,0 +1,40 @@ +@mixin igo-tour-theming($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + + igo-interactive-tour > button.mat-raised-button.tour-button-tool-icon { + box-shadow: none; + } + + igo-interactive-tour > button.tour-button-tool-icon { + background-color: mat-color($primary); + color: mat-color($primary, default-contrast); + + box-shadow: none; + border:none; + border-radius: 50%; + padding: 0; + min-width: 0; + width: 40px; + height: 40px; + flex-shrink: 0; + line-height: 40px; + } + + igo-interactive-tour > button.tour-button-tool-icon span.interactive-tour-button-title { + display: none; + } + + igo-interactive-tour > button.tour-button-tool { + background-color: mat-color($primary); + color: mat-color($primary, default-contrast); + border: none; + } + + igo-interactive-tour > button.tour-button-tool-icon:hover { + background-color: mat-color($primary, default-contrast); + color: mat-color($primary); + cursor: pointer; + } +} diff --git a/packages/common/src/lib/panel/panel.component.html b/packages/common/src/lib/panel/panel.component.html index 8aa7b477b5..b242aabf5f 100644 --- a/packages/common/src/lib/panel/panel.component.html +++ b/packages/common/src/lib/panel/panel.component.html @@ -2,7 +2,7 @@

- {{title}} + {{ title }}
diff --git a/packages/common/src/lib/panel/panel.component.ts b/packages/common/src/lib/panel/panel.component.ts index 956850e67b..a3cf7f7042 100644 --- a/packages/common/src/lib/panel/panel.component.ts +++ b/packages/common/src/lib/panel/panel.component.ts @@ -12,7 +12,6 @@ import { changeDetection: ChangeDetectionStrategy.OnPush }) export class PanelComponent { - @Input() get title() { return this._title; @@ -31,6 +30,4 @@ export class PanelComponent { this._withHeader = value; } private _withHeader = true; - - constructor() {} } diff --git a/packages/common/src/lib/panel/panel.module.ts b/packages/common/src/lib/panel/panel.module.ts index f1ce32fde9..5c920cea73 100644 --- a/packages/common/src/lib/panel/panel.module.ts +++ b/packages/common/src/lib/panel/panel.module.ts @@ -3,14 +3,8 @@ import { CommonModule } from '@angular/common'; import { PanelComponent } from './panel.component'; @NgModule({ - imports: [ - CommonModule - ], - exports: [ - PanelComponent - ], - declarations: [ - PanelComponent - ] + imports: [CommonModule], + exports: [PanelComponent], + declarations: [PanelComponent] }) export class IgoPanelModule {} diff --git a/packages/common/src/lib/tool/shared/tool.service.ts b/packages/common/src/lib/tool/shared/tool.service.ts index 0bf008c917..083b61536e 100644 --- a/packages/common/src/lib/tool/shared/tool.service.ts +++ b/packages/common/src/lib/tool/shared/tool.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { Tool } from './tool.interface'; +import { Toolbox } from './toolbox'; /** * Service where runtime tool configurations are registered @@ -9,14 +10,20 @@ import { Tool } from './tool.interface'; providedIn: 'root' }) export class ToolService { + static tools: { [key: string]: Tool } = {}; - static tools: {[key: string]: Tool} = {}; + /** + * Toolbox that holds main tools + */ + public toolbox: Toolbox = new Toolbox(); static register(tool: Tool) { ToolService.tools[tool.name] = tool; } - constructor() {} + constructor() { + this.toolbox.setTools(this.getTools()); + } /** * Return a tool @@ -34,5 +41,4 @@ export class ToolService { getTools(): Tool[] { return Object.values(ToolService.tools); } - } diff --git a/packages/common/src/locale/en.common.json b/packages/common/src/locale/en.common.json index 9cfc045e54..d87f0d4364 100644 --- a/packages/common/src/locale/en.common.json +++ b/packages/common/src/locale/en.common.json @@ -17,6 +17,13 @@ "actionbar": { "scrollUp": "Scroll up", "scrollDown": "Scroll down" + }, + "interactiveTour": { + "tooltipTourToolButton": "Interactive tour of the application", + "buttonTitle": "Discover", + "exitButton": "Exit", + "backButton": "Back", + "nextButton": "Next" } } } diff --git a/packages/common/src/locale/fr.common.json b/packages/common/src/locale/fr.common.json index bf8ec03138..006af584ae 100644 --- a/packages/common/src/locale/fr.common.json +++ b/packages/common/src/locale/fr.common.json @@ -17,6 +17,13 @@ "actionbar": { "scrollUp": "Défiler vers le haut", "scrollDown": "Défiler vers le bas" + }, + "interactiveTour": { + "tooltipTourToolButton": "Aide interactive sur l'outil", + "buttonTitle": "Découvrir", + "exitButton": "Quitter", + "backButton": "Précédent", + "nextButton": "Suivant" } } } diff --git a/packages/common/src/public_api.ts b/packages/common/src/public_api.ts index a84ac7e25c..e1a97dfd4e 100644 --- a/packages/common/src/public_api.ts +++ b/packages/common/src/public_api.ts @@ -24,6 +24,7 @@ export * from './lib/entity/entity.module'; export * from './lib/entity/entity-selector/entity-selector.module'; export * from './lib/entity/entity-table/entity-table.module'; export * from './lib/image/image.module'; +export * from './lib/interactive-tour/interactive-tour.module'; export * from './lib/json-dialog/json-dialog.module'; export * from './lib/keyvalue/keyvalue.module'; export * from './lib/list/list.module'; @@ -55,6 +56,7 @@ export * from './lib/form'; export * from './lib/entity'; export * from './lib/flexible'; export * from './lib/image'; +export * from './lib/interactive-tour'; export * from './lib/json-dialog'; export * from './lib/keyvalue'; export * from './lib/list'; diff --git a/packages/common/src/style/common.theming.scss b/packages/common/src/style/common.theming.scss index 261bdcc334..c96fd5cc16 100644 --- a/packages/common/src/style/common.theming.scss +++ b/packages/common/src/style/common.theming.scss @@ -1,8 +1,11 @@ +@import '~shepherd.js/dist/css/shepherd'; + @import '../lib/action/action.theming'; @import '../lib/collapsible/collapsible.theming'; @import '../lib/entity/entity.theming'; @import '../lib/list/list.theming'; @import '../lib/panel/panel.theming'; +@import '../lib/interactive-tour/interactive-tour.theming'; @import '../lib/tool/tool.theming'; @mixin igo-common-theming($theme, $typography) { @@ -12,4 +15,5 @@ @include igo-list-theming($theme); @include igo-panel-theming($theme); @include igo-tool-theming($theme); + @include igo-tour-theming($theme); } diff --git a/packages/integration/src/lib/about/about-tool/about-tool.component.html b/packages/integration/src/lib/about/about-tool/about-tool.component.html index 0b11055561..def3f5eaf8 100644 --- a/packages/integration/src/lib/about/about-tool/about-tool.component.html +++ b/packages/integration/src/lib/about/about-tool/about-tool.component.html @@ -1,3 +1,11 @@ +

+ + + + [html]="html |  translate: {version: version}"> diff --git a/packages/integration/src/lib/about/about-tool/about-tool.component.scss b/packages/integration/src/lib/about/about-tool/about-tool.component.scss new file mode 100644 index 0000000000..38f55dcc1b --- /dev/null +++ b/packages/integration/src/lib/about/about-tool/about-tool.component.scss @@ -0,0 +1,3 @@ +igo-interactive-tour { + margin-left: 20px; +} diff --git a/packages/integration/src/lib/about/about-tool/about-tool.component.ts b/packages/integration/src/lib/about/about-tool/about-tool.component.ts index 1f49d6b29e..c683aa4402 100644 --- a/packages/integration/src/lib/about/about-tool/about-tool.component.ts +++ b/packages/integration/src/lib/about/about-tool/about-tool.component.ts @@ -10,7 +10,8 @@ import { ConfigService, Version } from '@igo2/core'; }) @Component({ selector: 'igo-about-tool', - templateUrl: './about-tool.component.html' + templateUrl: './about-tool.component.html', + styleUrls: ['./about-tool.component.scss'] }) export class AboutToolComponent { @Input() diff --git a/packages/integration/src/lib/about/about.module.ts b/packages/integration/src/lib/about/about.module.ts index e945886919..e56dc1e275 100644 --- a/packages/integration/src/lib/about/about.module.ts +++ b/packages/integration/src/lib/about/about.module.ts @@ -3,14 +3,24 @@ import { ModuleWithProviders, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; import { IgoLanguageModule } from '@igo2/core'; -import { IgoCustomHtmlModule } from '@igo2/common'; +import { IgoCustomHtmlModule, IgoInteractiveTourModule } from '@igo2/common'; import { AboutToolComponent } from './about-tool/about-tool.component'; @NgModule({ - imports: [IgoLanguageModule, IgoCustomHtmlModule], + imports: [ + IgoLanguageModule, + IgoCustomHtmlModule, + MatButtonModule, + MatTooltipModule, + MatIconModule, + IgoInteractiveTourModule + ], declarations: [AboutToolComponent], exports: [AboutToolComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/packages/integration/src/lib/context/context-editor-tool/context-editor-tool.component.html b/packages/integration/src/lib/context/context-editor-tool/context-editor-tool.component.html index 8c5f05f78e..8127980877 100644 --- a/packages/integration/src/lib/context/context-editor-tool/context-editor-tool.component.html +++ b/packages/integration/src/lib/context/context-editor-tool/context-editor-tool.component.html @@ -1 +1,4 @@ - + diff --git a/packages/integration/src/lib/search/search-bar/search-bar.module.ts b/packages/integration/src/lib/search/search-bar/search-bar.module.ts index e82fd2c619..0374cbcb84 100644 --- a/packages/integration/src/lib/search/search-bar/search-bar.module.ts +++ b/packages/integration/src/lib/search/search-bar/search-bar.module.ts @@ -8,10 +8,8 @@ import { SearchBarBindingDirective } from './search-bar-binding.directive'; * @ignore */ @NgModule({ - imports: [ - IgoSearchModule - ], + imports: [IgoSearchModule], declarations: [SearchBarBindingDirective], - exports: [SearchBarBindingDirective] + exports: [SearchBarBindingDirective], }) export class IgoAppSearchBarModule {} diff --git a/packages/integration/src/lib/tool/tool.state.ts b/packages/integration/src/lib/tool/tool.state.ts index 8543aca406..1fd2ceb346 100644 --- a/packages/integration/src/lib/tool/tool.state.ts +++ b/packages/integration/src/lib/tool/tool.state.ts @@ -9,12 +9,9 @@ import { Toolbox, ToolService } from '@igo2/common'; providedIn: 'root' }) export class ToolState { - /** - * Toolbox that holds main tools - */ - toolbox: Toolbox = new Toolbox(); - - constructor(private toolService: ToolService) { - this.toolbox.setTools(this.toolService.getTools()); + get toolbox(): Toolbox { + return this.toolService.toolbox; } + + constructor(private toolService: ToolService) {} }