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 @@
+
+ {{'igo.common.interactiveTour.buttonTitle' | translate}} {{'IGO' | translate}}
+
+
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) {}
}