Guided Tour added
This commit is contained in:
parent
70d2d456ee
commit
b36621877e
|
@ -27,7 +27,8 @@
|
|||
"src/styles.scss",
|
||||
"src/assets/scss/material-dashboard.scss",
|
||||
"src/assets/css/demo.css",
|
||||
"node_modules/cookieconsent/build/cookieconsent.min.css"
|
||||
"node_modules/cookieconsent/build/cookieconsent.min.css",
|
||||
"src/app/library/guided-tour/guided-tour-base-theme.scss"
|
||||
],
|
||||
"scripts": [
|
||||
"node_modules/cookieconsent/build/cookieconsent.min.js",
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"moment-timezone": "^0.5.26",
|
||||
"ngx-cookie-service": "^2.2.0",
|
||||
"ngx-cookieconsent": "^2.2.3",
|
||||
"ngx-guided-tour": "^1.1.10",
|
||||
"rxjs": "^6.3.2",
|
||||
"tinymce": "^5.1.6",
|
||||
"tslib": "^1.10.0",
|
||||
|
|
|
@ -24,3 +24,5 @@
|
|||
</div>
|
||||
<app-notification *ngIf="!onlySplash"></app-notification>
|
||||
<router-outlet *ngIf="onlySplash"></router-outlet>
|
||||
|
||||
<ngx-guided-tour></ngx-guided-tour>
|
|
@ -32,6 +32,7 @@ import { BaseHttpService } from './core/services/http/base-http.service';
|
|||
import { ConfigurationService } from './core/services/configuration/configuration.service';
|
||||
import { Oauth2DialogModule } from './ui/misc/oauth2-dialog/oauth2-dialog.module';
|
||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS, MatFormFieldDefaultOptions } from '@angular/material';
|
||||
import { GuidedTourModule } from './library/guided-tour/guided-tour.module';
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
export function HttpLoaderFactory(http: HttpClient, appConfig: ConfigurationService) {
|
||||
|
@ -105,7 +106,8 @@ const appearance: MatFormFieldDefaultOptions = {
|
|||
NavbarModule,
|
||||
SidebarModule,
|
||||
NgcCookieConsentModule.forRoot(cookieConfig),
|
||||
Oauth2DialogModule
|
||||
Oauth2DialogModule,
|
||||
GuidedTourModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
$tour-zindex: 1081 !default;
|
||||
$tour-step-color: #ffffff !default;
|
||||
$tour-text-color: #231f1f !default;
|
||||
$tour-next-button-color: #007bff !default;
|
||||
$tour-next-button-hover: #0069d9 !default;
|
||||
$tour-back-button-color: #007bff !default;
|
||||
$tour-next-text-color: #ffffff !default;
|
||||
$tour-next-text-hover: #ffffff !default;
|
||||
$tour-skip-link-color: #5e5e5e !default;
|
||||
$tour-orb-color: #625aff !default;
|
||||
$tour-shadow-color: #4c4c4c !default;
|
||||
|
||||
body.tour-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@mixin tour-triangle($direction, $color: currentColor, $size: 1rem) {
|
||||
|
||||
@if not index(top right bottom left, $direction) {
|
||||
@error 'Direction must be either `top`, `right`, `bottom` or `left`.';
|
||||
}
|
||||
|
||||
$opposite-direction: top;
|
||||
|
||||
@if $direction==top {
|
||||
$opposite-direction: bottom;
|
||||
}
|
||||
|
||||
@if $direction==bottom {
|
||||
$opposite-direction: top;
|
||||
}
|
||||
|
||||
@if $direction==right {
|
||||
$opposite-direction: left;
|
||||
}
|
||||
|
||||
@if $direction==left {
|
||||
$opposite-direction: right;
|
||||
}
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
content: '';
|
||||
z-index: 2;
|
||||
border-#{$opposite-direction}: $size solid $color;
|
||||
$perpendicular-borders: $size solid transparent;
|
||||
@if $direction==top or $direction==bottom {
|
||||
border-left: $perpendicular-borders;
|
||||
border-right: $perpendicular-borders;
|
||||
}
|
||||
@else if $direction==right or $direction==left {
|
||||
border-bottom: $perpendicular-borders;
|
||||
border-top: $perpendicular-borders;
|
||||
}
|
||||
}
|
||||
|
||||
ngx-guided-tour {
|
||||
.guided-tour-user-input-mask {
|
||||
z-index: $tour-zindex;
|
||||
}
|
||||
|
||||
.guided-tour-spotlight-overlay {
|
||||
z-index: $tour-zindex + 1;
|
||||
}
|
||||
|
||||
.tour-orb {
|
||||
z-index: $tour-zindex - 2;
|
||||
background-color: $tour-orb-color;
|
||||
box-shadow: 0 0 0.3rem 0.1rem $tour-orb-color;
|
||||
|
||||
.tour-orb-ring {
|
||||
&::after {
|
||||
border: 1rem solid $tour-orb-color;
|
||||
box-shadow: 0 0 0.1rem 0.1rem $tour-orb-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tour-step {
|
||||
z-index: $tour-zindex + 2;
|
||||
|
||||
&.tour-bottom, &.tour-bottom-right, &.tour-bottom-left {
|
||||
.tour-arrow::before {
|
||||
@include tour-triangle(top, $tour-step-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.tour-top, &.tour-top-right, &.tour-top-left {
|
||||
.tour-arrow::before {
|
||||
@include tour-triangle(bottom, $tour-step-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.tour-left {
|
||||
.tour-arrow::before {
|
||||
@include tour-triangle(right, $tour-step-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.tour-right {
|
||||
.tour-arrow::before {
|
||||
@include tour-triangle(left, $tour-step-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tour-block {
|
||||
color: $tour-text-color;
|
||||
background-color: $tour-step-color;
|
||||
box-shadow: 0 0.4rem 0.6rem $tour-shadow-color;
|
||||
}
|
||||
|
||||
.tour-buttons {
|
||||
button.skip-button {
|
||||
color: $tour-skip-link-color;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
color: $tour-back-button-color;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
background-color: $tour-next-button-color;
|
||||
color: $tour-next-text-color;
|
||||
&:hover {
|
||||
background-color: $tour-next-button-hover;
|
||||
color: $tour-next-text-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
<!-- <div *ngIf="currentTourStep && selectedElementRect && isOrbShowing"y
|
||||
class="tour-orb tour-{{ currentTourStep.orientation }}"
|
||||
(mouseenter)="handleOrb()"
|
||||
[style.top.px]="orbTopPosition"
|
||||
[style.left.px]="orbLeftPosition"
|
||||
[style.transform]="orbTransform">
|
||||
<div class="tour-orb-ring"></div>
|
||||
</div> -->
|
||||
<div *ngIf="currentTourStep && !isOrbShowing">
|
||||
<div class="guided-tour-user-input-mask" (click)="backdropClick($event)"></div>
|
||||
<div class="guided-tour-spotlight-overlay" [style.top.px]="overlayTop" [style.left.px]="overlayLeft"
|
||||
[style.height.px]="overlayHeight" [style.width.px]="overlayWidth">
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="currentTourStep && !isOrbShowing">
|
||||
<div #tourStep *ngIf="currentTourStep" class="tour-step tour-{{ currentTourStep.orientation }}" [ngClass]="{
|
||||
'page-tour-step': !currentTourStep.selector
|
||||
}" [style.top.px]="(currentTourStep.selector && selectedElementRect ? topPosition : null)"
|
||||
[style.left.px]="(currentTourStep.selector && selectedElementRect ? leftPosition : null)"
|
||||
[style.width.px]="(currentTourStep.selector && selectedElementRect ? calculatedTourStepWidth : null)"
|
||||
[style.transform]="(currentTourStep.selector && selectedElementRect ? transform : null)">
|
||||
<div *ngIf="currentTourStep.selector" class="tour-arrow"></div>
|
||||
<div class="tour-block">
|
||||
<!-- <div *ngIf="
|
||||
progressIndicatorLocation === progressIndicatorLocations.TopOfTourBlock
|
||||
&& !guidedTourService.onResizeMessage" class="tour-progress-indicator">
|
||||
<ng-container *ngTemplateOutlet="progress"></ng-container>
|
||||
</div> -->
|
||||
<h3 class="tour-title" *ngIf="currentTourStep.title && currentTourStep.selector">
|
||||
{{currentTourStep.title}}
|
||||
</h3>
|
||||
<h2 class="tour-title" *ngIf="currentTourStep.title && !currentTourStep.selector">
|
||||
{{ currentTourStep.title }}
|
||||
</h2>
|
||||
<!-- <div class="tour-content" [innerHTML]="currentTourStep.content"></div> -->
|
||||
<div class="tour-buttons">
|
||||
<button *ngIf="!guidedTourService.onResizeMessage" class="next-button"
|
||||
(click)="guidedTourService.nextStep()">
|
||||
{{ nextText }}
|
||||
</button>
|
||||
<button *ngIf="!guidedTourService.onResizeMessage" (click)="guidedTourService.skipTour()"
|
||||
class="skip-button link-button">
|
||||
{{ skipText }}
|
||||
</button>
|
||||
<!-- <button *ngIf="!guidedTourService.onResizeMessage"
|
||||
(click)="guidedTourService.skipTour()"
|
||||
class="skip-button link-button">
|
||||
{{ skipText }}
|
||||
</button> -->
|
||||
<!-- <button *ngIf="!guidedTourService.onLastStep && !guidedTourService.onResizeMessage"
|
||||
class="next-button"
|
||||
(click)="guidedTourService.nextStep()">
|
||||
{{ nextText }}
|
||||
<ng-container *ngIf="progressIndicatorLocation === progressIndicatorLocations.InsideNextButton">
|
||||
<ng-container *ngTemplateOutlet="progress"></ng-container>
|
||||
</ng-container>
|
||||
</button> -->
|
||||
<!-- <button *ngIf="guidedTourService.onLastStep"
|
||||
class="next-button"
|
||||
(click)="guidedTourService.nextStep()">
|
||||
{{ doneText }}
|
||||
</button> -->
|
||||
<!-- <button *ngIf="guidedTourService.onResizeMessage"
|
||||
class="next-button"
|
||||
(click)="guidedTourService.resetTour()">
|
||||
{{ closeText }}
|
||||
</button> -->
|
||||
<!-- <button *ngIf="!guidedTourService.onFirstStep && !guidedTourService.onResizeMessage"
|
||||
class="back-button link-button"
|
||||
(click)="guidedTourService.backStep()">
|
||||
{{ backText }}
|
||||
</button> -->
|
||||
</div>
|
||||
<div class="argos-present-img"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <ng-template #progress>
|
||||
<ng-container *ngTemplateOutlet="
|
||||
progressIndicator || defaultProgressIndicator;
|
||||
context: { currentStepNumber: guidedTourService.currentTourStepDisplay, totalSteps: guidedTourService.currentTourStepCount }
|
||||
"></ng-container>
|
||||
</ng-template>
|
||||
<ng-template #defaultProgressIndicator let-currentStepNumber="currentStepNumber" let-totalSteps="totalSteps">
|
||||
<ng-container *ngIf="progressIndicatorLocation === progressIndicatorLocations.InsideNextButton"> </ng-container>{{ currentStepNumber }}/{{ totalSteps }}
|
||||
</ng-template> -->
|
|
@ -0,0 +1,235 @@
|
|||
ngx-guided-tour {
|
||||
.guided-tour-user-input-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-height: 100vh;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.guided-tour-spotlight-overlay {
|
||||
position: fixed;
|
||||
box-shadow: 0 0 0 9999px rgba(0,0,0,.7), 0 0 1.5rem rgba(0,0,0,.5);
|
||||
border-radius: 44px;
|
||||
}
|
||||
|
||||
.tour-orb {
|
||||
position: fixed;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
|
||||
.tour-orb-ring {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: pulse 2s linear infinite;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
transform: translate(-50%, -50%) scale(0.45);
|
||||
opacity: 1.0;
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tour-step {
|
||||
position: fixed;
|
||||
&.page-tour-step {
|
||||
max-width: 1043px;
|
||||
width: 50%;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
border-radius: 5px;
|
||||
transform: translate(-50%, -50%)
|
||||
}
|
||||
&.tour-bottom, &.tour-bottom-right, &.tour-bottom-left {
|
||||
.tour-arrow::before {
|
||||
position: absolute;
|
||||
}
|
||||
.tour-block {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.tour-top, &.tour-top-right, &.tour-top-left {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.tour-arrow::before {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
.tour-block {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.tour-bottom , &.tour-top {
|
||||
.tour-arrow::before {
|
||||
transform: translateX(-50%);
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.tour-bottom-right, &.tour-top-right {
|
||||
.tour-arrow::before {
|
||||
transform: translateX(-100%);
|
||||
left: calc(100% - 5px);
|
||||
}
|
||||
}
|
||||
|
||||
&.tour-bottom-left, &.tour-top-left {
|
||||
.tour-arrow::before {
|
||||
left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.tour-left {
|
||||
.tour-arrow::before {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
transform: translateX(-100%);
|
||||
top: 5px;
|
||||
}
|
||||
.tour-block {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.tour-right {
|
||||
.tour-arrow::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 5px;
|
||||
}
|
||||
.tour-block {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.tour-block {
|
||||
padding: 15px 25px;
|
||||
height: 348px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.tour-progress-indicator {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.tour-title {
|
||||
font-weight: lighter !important;
|
||||
font-size: 16px !important;
|
||||
padding-left: 65px;
|
||||
padding-top: 28px;
|
||||
padding-right: 156px;
|
||||
// padding-bottom: 20px;
|
||||
text-align: left;
|
||||
color: #212121;
|
||||
line-height: 26px;
|
||||
white-space:pre-line;
|
||||
height: 182px;
|
||||
}
|
||||
|
||||
h3.tour-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
h2.tour-title {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.tour-content {
|
||||
min-height: 80px;
|
||||
padding-bottom: 30px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.tour-buttons {
|
||||
overflow: hidden; // clearfix
|
||||
padding: 40px 156px 25px 65px;
|
||||
|
||||
button.link-button {
|
||||
padding-left: 0;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
max-width: none !important;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
border: 1px solid transparent;
|
||||
line-height: 1.5;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
outline: none;
|
||||
padding: 0 15px;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button.skip-button.link-button {
|
||||
padding: 0;
|
||||
border-left: 0;
|
||||
float: right;
|
||||
width: 133px;
|
||||
height: 40px;
|
||||
border: 1px solid #129D99;
|
||||
background: #FFFFFF 0% 0% no-repeat padding-box;
|
||||
color: #129D99;
|
||||
// text-align: center;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 10px 0px;;
|
||||
width: 101px;
|
||||
background: #129D99 0% 0% no-repeat padding-box;
|
||||
}
|
||||
|
||||
button.skip-button.link-button, .next-button {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.35px;
|
||||
height: 40px;
|
||||
box-shadow: 0px 3px 6px #1E202029;
|
||||
border-radius: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.argos-present-img {
|
||||
background: url("../../../assets/splash/assets/img/argos\ present.png") no-repeat;
|
||||
width: 176px;
|
||||
height: 220px;
|
||||
position: relative;
|
||||
top: -205px;
|
||||
left: 820px;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,392 @@
|
|||
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild, ViewEncapsulation, TemplateRef, Inject } from '@angular/core';
|
||||
import { fromEvent, Subscription } from 'rxjs';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Orientation, TourStep, ProgressIndicatorLocation } from './guided-tour.constants';
|
||||
import { GuidedTourService } from './guided-tour.service';
|
||||
import { WindowRefService } from "./windowref.service";
|
||||
|
||||
@Component({
|
||||
selector: 'ngx-guided-tour',
|
||||
templateUrl: './guided-tour.component.html',
|
||||
styleUrls: ['./guided-tour.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class GuidedTourComponent implements AfterViewInit, OnDestroy {
|
||||
@Input() public topOfPageAdjustment ?= 0;
|
||||
@Input() public tourStepWidth ?= 1043;
|
||||
@Input() public minimalTourStepWidth ?= 900;
|
||||
@Input() public skipText ?= 'Leave Tour';
|
||||
@Input() public nextText ?= 'Got it!';
|
||||
@Input() public doneText ?= 'Done';
|
||||
@Input() public closeText ?= 'Close';
|
||||
@Input() public backText ?= 'Back';
|
||||
@Input() public progressIndicatorLocation?: ProgressIndicatorLocation = ProgressIndicatorLocation.InsideNextButton;
|
||||
@Input() public progressIndicator?: TemplateRef<any> = undefined;
|
||||
@ViewChild('tourStep', { static: false }) public tourStep: ElementRef;
|
||||
public highlightPadding = 4;
|
||||
public currentTourStep: TourStep = null;
|
||||
public selectedElementRect: DOMRect = null;
|
||||
public isOrbShowing = false;
|
||||
public progressIndicatorLocations = ProgressIndicatorLocation;
|
||||
|
||||
private resizeSubscription: Subscription;
|
||||
private scrollSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
public guidedTourService: GuidedTourService,
|
||||
private windowRef: WindowRefService,
|
||||
@Inject(DOCUMENT) private dom: any
|
||||
) { }
|
||||
|
||||
private get maxWidthAdjustmentForTourStep(): number {
|
||||
return this.tourStepWidth - this.minimalTourStepWidth;
|
||||
}
|
||||
|
||||
private get widthAdjustmentForScreenBound(): number {
|
||||
if (!this.tourStep) {
|
||||
return 0;
|
||||
}
|
||||
let adjustment = 0;
|
||||
if (this.calculatedLeftPosition < 0) {
|
||||
adjustment = -this.calculatedLeftPosition;
|
||||
}
|
||||
if (this.calculatedLeftPosition > this.windowRef.nativeWindow.innerWidth - this.tourStepWidth) {
|
||||
adjustment = this.calculatedLeftPosition - (this.windowRef.nativeWindow.innerWidth - this.tourStepWidth);
|
||||
}
|
||||
|
||||
return Math.min(this.maxWidthAdjustmentForTourStep, adjustment);
|
||||
}
|
||||
|
||||
public get calculatedTourStepWidth() {
|
||||
return this.tourStepWidth - this.widthAdjustmentForScreenBound;
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
this.guidedTourService.guidedTourCurrentStepStream.subscribe((step: TourStep) => {
|
||||
this.currentTourStep = step;
|
||||
if (step && step.selector) {
|
||||
const selectedElement = this.dom.querySelector(step.selector);
|
||||
if (selectedElement) {
|
||||
this.handleOrb();
|
||||
this.scrollToAndSetElement();
|
||||
} else {
|
||||
this.selectedElementRect = null;
|
||||
}
|
||||
} else {
|
||||
this.selectedElementRect = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.guidedTourService.guidedTourOrbShowingStream.subscribe((value: boolean) => {
|
||||
this.isOrbShowing = value;
|
||||
});
|
||||
|
||||
this.resizeSubscription = fromEvent(this.windowRef.nativeWindow, 'resize').subscribe(() => {
|
||||
this.updateStepLocation();
|
||||
});
|
||||
|
||||
this.scrollSubscription = fromEvent(this.windowRef.nativeWindow, 'scroll').subscribe(() => {
|
||||
this.updateStepLocation();
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.resizeSubscription.unsubscribe();
|
||||
this.scrollSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
public scrollToAndSetElement(): void {
|
||||
this.updateStepLocation();
|
||||
// Allow things to render to scroll to the correct location
|
||||
setTimeout(() => {
|
||||
if (!this.isOrbShowing && !this.isTourOnScreen()) {
|
||||
if (this.selectedElementRect && this.isBottom()) {
|
||||
// Scroll so the element is on the top of the screen.
|
||||
const topPos = ((this.windowRef.nativeWindow.scrollY + this.selectedElementRect.top) - this.topOfPageAdjustment)
|
||||
- (this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0)
|
||||
+ this.getStepScreenAdjustment();
|
||||
try {
|
||||
this.windowRef.nativeWindow.scrollTo({
|
||||
left: null,
|
||||
top: topPos,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
this.windowRef.nativeWindow.scroll(0, topPos);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Scroll so the element is on the bottom of the screen.
|
||||
const topPos = (this.windowRef.nativeWindow.scrollY + this.selectedElementRect.top + this.selectedElementRect.height)
|
||||
- this.windowRef.nativeWindow.innerHeight
|
||||
+ (this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0)
|
||||
- this.getStepScreenAdjustment();
|
||||
try {
|
||||
this.windowRef.nativeWindow.scrollTo({
|
||||
left: null,
|
||||
top: topPos,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
this.windowRef.nativeWindow.scroll(0, topPos);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public handleOrb(): void {
|
||||
this.guidedTourService.activateOrb();
|
||||
if (this.currentTourStep && this.currentTourStep.selector) {
|
||||
this.scrollToAndSetElement();
|
||||
}
|
||||
}
|
||||
|
||||
private isTourOnScreen(): boolean {
|
||||
return this.tourStep
|
||||
&& this.elementInViewport(this.dom.querySelector(this.currentTourStep.selector))
|
||||
&& this.elementInViewport(this.tourStep.nativeElement);
|
||||
}
|
||||
|
||||
private elementInViewport(element: HTMLElement): boolean {
|
||||
let top = element.offsetTop;
|
||||
const height = element.offsetHeight;
|
||||
|
||||
while (element.offsetParent) {
|
||||
element = (element.offsetParent as HTMLElement);
|
||||
top += element.offsetTop;
|
||||
}
|
||||
if (this.isBottom()) {
|
||||
return (
|
||||
top >= (this.windowRef.nativeWindow.pageYOffset
|
||||
+ this.topOfPageAdjustment
|
||||
+ (this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0)
|
||||
+ this.getStepScreenAdjustment())
|
||||
&& (top + height) <= (this.windowRef.nativeWindow.pageYOffset + this.windowRef.nativeWindow.innerHeight)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
top >= (this.windowRef.nativeWindow.pageYOffset + this.topOfPageAdjustment - this.getStepScreenAdjustment())
|
||||
&& (top + height + (this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0)) <= (this.windowRef.nativeWindow.pageYOffset + this.windowRef.nativeWindow.innerHeight)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public backdropClick(event: Event): void {
|
||||
if (this.guidedTourService.preventBackdropFromAdvancing) {
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
this.guidedTourService.nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
public updateStepLocation(): void {
|
||||
if (this.currentTourStep && this.currentTourStep.selector) {
|
||||
const selectedElement = this.dom.querySelector(this.currentTourStep.selector);
|
||||
if (selectedElement && typeof selectedElement.getBoundingClientRect === 'function') {
|
||||
this.selectedElementRect = (selectedElement.getBoundingClientRect() as DOMRect);
|
||||
} else {
|
||||
this.selectedElementRect = null;
|
||||
}
|
||||
} else {
|
||||
this.selectedElementRect = null;
|
||||
}
|
||||
}
|
||||
|
||||
private isBottom(): boolean {
|
||||
return this.currentTourStep.orientation
|
||||
&& (this.currentTourStep.orientation === Orientation.Bottom
|
||||
|| this.currentTourStep.orientation === Orientation.BottomLeft
|
||||
|| this.currentTourStep.orientation === Orientation.BottomRight);
|
||||
}
|
||||
|
||||
public get topPosition(): number {
|
||||
const paddingAdjustment = this.getHighlightPadding();
|
||||
|
||||
if (this.isBottom()) {
|
||||
return this.selectedElementRect.top + this.selectedElementRect.height + paddingAdjustment;
|
||||
}
|
||||
|
||||
return this.selectedElementRect.top - this.getHighlightPadding();
|
||||
}
|
||||
|
||||
public get orbTopPosition(): number {
|
||||
if (this.isBottom()) {
|
||||
return this.selectedElementRect.top + this.selectedElementRect.height;
|
||||
}
|
||||
|
||||
if (
|
||||
this.currentTourStep.orientation === Orientation.Right
|
||||
|| this.currentTourStep.orientation === Orientation.Left
|
||||
) {
|
||||
return (this.selectedElementRect.top + (this.selectedElementRect.height / 2));
|
||||
}
|
||||
|
||||
return this.selectedElementRect.top;
|
||||
}
|
||||
|
||||
private get calculatedLeftPosition(): number {
|
||||
const paddingAdjustment = this.getHighlightPadding();
|
||||
|
||||
if (
|
||||
this.currentTourStep.orientation === Orientation.TopRight
|
||||
|| this.currentTourStep.orientation === Orientation.BottomRight
|
||||
) {
|
||||
return (this.selectedElementRect.right - this.tourStepWidth);
|
||||
}
|
||||
|
||||
if (
|
||||
this.currentTourStep.orientation === Orientation.TopLeft
|
||||
|| this.currentTourStep.orientation === Orientation.BottomLeft
|
||||
) {
|
||||
return (this.selectedElementRect.left);
|
||||
}
|
||||
|
||||
if (this.currentTourStep.orientation === Orientation.Left) {
|
||||
return this.selectedElementRect.left - this.tourStepWidth - paddingAdjustment;
|
||||
}
|
||||
|
||||
if (this.currentTourStep.orientation === Orientation.Right) {
|
||||
return (this.selectedElementRect.left + this.selectedElementRect.width + paddingAdjustment);
|
||||
}
|
||||
|
||||
return (this.selectedElementRect.right - (this.selectedElementRect.width / 2) - (this.tourStepWidth / 2));
|
||||
}
|
||||
|
||||
public get leftPosition(): number {
|
||||
if (this.calculatedLeftPosition >= 0) {
|
||||
return this.calculatedLeftPosition;
|
||||
}
|
||||
const adjustment = Math.max(0, -this.calculatedLeftPosition)
|
||||
const maxAdjustment = Math.min(this.maxWidthAdjustmentForTourStep, adjustment);
|
||||
return this.calculatedLeftPosition + maxAdjustment;
|
||||
}
|
||||
|
||||
public get orbLeftPosition(): number {
|
||||
if (
|
||||
this.currentTourStep.orientation === Orientation.TopRight
|
||||
|| this.currentTourStep.orientation === Orientation.BottomRight
|
||||
) {
|
||||
return this.selectedElementRect.right;
|
||||
}
|
||||
|
||||
if (
|
||||
this.currentTourStep.orientation === Orientation.TopLeft
|
||||
|| this.currentTourStep.orientation === Orientation.BottomLeft
|
||||
) {
|
||||
return this.selectedElementRect.left;
|
||||
}
|
||||
|
||||
if (this.currentTourStep.orientation === Orientation.Left) {
|
||||
return this.selectedElementRect.left;
|
||||
}
|
||||
|
||||
if (this.currentTourStep.orientation === Orientation.Right) {
|
||||
return (this.selectedElementRect.left + this.selectedElementRect.width);
|
||||
}
|
||||
|
||||
return (this.selectedElementRect.right - (this.selectedElementRect.width / 2));
|
||||
}
|
||||
|
||||
public get transform(): string {
|
||||
if (
|
||||
!this.currentTourStep.orientation
|
||||
|| this.currentTourStep.orientation === Orientation.Top
|
||||
|| this.currentTourStep.orientation === Orientation.TopRight
|
||||
|| this.currentTourStep.orientation === Orientation.TopLeft
|
||||
) {
|
||||
return 'translateY(-100%)';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public get orbTransform(): string {
|
||||
if (
|
||||
!this.currentTourStep.orientation
|
||||
|| this.currentTourStep.orientation === Orientation.Top
|
||||
|| this.currentTourStep.orientation === Orientation.Bottom
|
||||
|| this.currentTourStep.orientation === Orientation.TopLeft
|
||||
|| this.currentTourStep.orientation === Orientation.BottomLeft
|
||||
) {
|
||||
return 'translateY(-50%)';
|
||||
}
|
||||
|
||||
if (
|
||||
this.currentTourStep.orientation === Orientation.TopRight
|
||||
|| this.currentTourStep.orientation === Orientation.BottomRight
|
||||
) {
|
||||
return 'translate(-100%, -50%)';
|
||||
}
|
||||
|
||||
if (
|
||||
this.currentTourStep.orientation === Orientation.Right
|
||||
|| this.currentTourStep.orientation === Orientation.Left
|
||||
) {
|
||||
return 'translate(-50%, -50%)';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public get overlayTop(): number {
|
||||
if (this.selectedElementRect) {
|
||||
return this.selectedElementRect.top - this.getHighlightPadding();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public get overlayLeft(): number {
|
||||
if (this.selectedElementRect) {
|
||||
return this.selectedElementRect.left - this.getHighlightPadding();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public get overlayHeight(): number {
|
||||
if (this.selectedElementRect) {
|
||||
return this.selectedElementRect.height + (this.getHighlightPadding() * 2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public get overlayWidth(): number {
|
||||
if (this.selectedElementRect) {
|
||||
return (this.selectedElementRect.width + (this.getHighlightPadding() * 2)) * 0.95;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private getHighlightPadding(): number {
|
||||
let paddingAdjustment = this.currentTourStep.useHighlightPadding ? this.highlightPadding : 0;
|
||||
if (this.currentTourStep.highlightPadding) {
|
||||
paddingAdjustment = this.currentTourStep.highlightPadding;
|
||||
}
|
||||
return paddingAdjustment;
|
||||
}
|
||||
|
||||
// This calculates a value to add or subtract so the step should not be off screen.
|
||||
private getStepScreenAdjustment(): number {
|
||||
if (
|
||||
this.currentTourStep.orientation === Orientation.Left
|
||||
|| this.currentTourStep.orientation === Orientation.Right
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const scrollAdjustment = this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0;
|
||||
const tourStepHeight = typeof this.tourStep.nativeElement.getBoundingClientRect === 'function' ? this.tourStep.nativeElement.getBoundingClientRect().height : 0;
|
||||
const elementHeight = this.selectedElementRect.height + scrollAdjustment + tourStepHeight;
|
||||
|
||||
if ((this.windowRef.nativeWindow.innerHeight - this.topOfPageAdjustment) < elementHeight) {
|
||||
return elementHeight - (this.windowRef.nativeWindow.innerHeight - this.topOfPageAdjustment);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
|
||||
export interface TourStep {
|
||||
/** Selector for element that will be highlighted */
|
||||
selector?: string;
|
||||
/** Tour title text */
|
||||
title?: string;
|
||||
/** Tour step text */
|
||||
content: string;
|
||||
/** Where the tour step will appear next to the selected element */
|
||||
orientation?: Orientation | OrientationConfiguration[];
|
||||
/** Action that happens when the step is opened */
|
||||
action?: () => void;
|
||||
/** Action that happens when the step is closed */
|
||||
closeAction?: () => void;
|
||||
/** Skips this step, this is so you do not have create multiple tour configurations based on user settings/configuration */
|
||||
skipStep?: boolean;
|
||||
/** Adds some padding for things like sticky headers when scrolling to an element */
|
||||
scrollAdjustment?: number;
|
||||
/** Adds default padding around tour highlighting. Does not need to be true for highlightPadding to work */
|
||||
useHighlightPadding?: boolean;
|
||||
/** Adds padding around tour highlighting in pixels, this overwrites the default for this step. Is not dependent on useHighlightPadding being true */
|
||||
highlightPadding?: number;
|
||||
}
|
||||
|
||||
export interface GuidedTour {
|
||||
/** Identifier for tour */
|
||||
tourId: string;
|
||||
/** Use orb to start tour */
|
||||
useOrb?: boolean;
|
||||
/** Steps fo the tour */
|
||||
steps: TourStep[];
|
||||
/** Function will be called when tour is skipped */
|
||||
skipCallback?: (stepSkippedOn: number) => void;
|
||||
/** Function will be called when tour is completed */
|
||||
completeCallback?: () => void;
|
||||
/** Minimum size of screen in pixels before the tour is run, if the tour is resized below this value the user will be told to resize */
|
||||
minimumScreenSize?: number;
|
||||
/** Dialog shown if the window width is smaller than the defined minimum screen size. */
|
||||
resizeDialog?: {
|
||||
/** Resize dialog title text */
|
||||
title?: string;
|
||||
/** Resize dialog text */
|
||||
content: string;
|
||||
}
|
||||
/**
|
||||
* Prevents the tour from advancing by clicking the backdrop.
|
||||
* This should only be set if you are completely sure your tour is displaying correctly on all screen sizes otherwise a user can get stuck.
|
||||
*/
|
||||
preventBackdropFromAdvancing?: boolean;
|
||||
}
|
||||
|
||||
export interface OrientationConfiguration {
|
||||
/** Where the tour step will appear next to the selected element */
|
||||
orientationDirection: Orientation;
|
||||
/** When this orientation configuration starts in pixels */
|
||||
maximumSize?: number;
|
||||
}
|
||||
|
||||
export class Orientation {
|
||||
public static readonly Bottom = 'bottom';
|
||||
public static readonly BottomLeft = 'bottom-left';
|
||||
public static readonly BottomRight = 'bottom-right';
|
||||
public static readonly Center = 'center';
|
||||
public static readonly Left = 'left';
|
||||
public static readonly Right = 'right';
|
||||
public static readonly Top = 'top';
|
||||
public static readonly TopLeft = 'top-left';
|
||||
public static readonly TopRight = 'top-right';
|
||||
}
|
||||
|
||||
export enum ProgressIndicatorLocation {
|
||||
InsideNextButton = 'inside-next-button',
|
||||
TopOfTourBlock = 'top-of-tour-block',
|
||||
None = 'none',
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { GuidedTourService } from './guided-tour.service';
|
||||
import { GuidedTourComponent } from './guided-tour.component';
|
||||
import { NgModule, ErrorHandler, ModuleWithProviders } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { WindowRefService } from './windowref.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [GuidedTourComponent],
|
||||
imports: [CommonModule],
|
||||
providers: [WindowRefService],
|
||||
exports: [GuidedTourComponent],
|
||||
entryComponents: [GuidedTourComponent],
|
||||
})
|
||||
export class GuidedTourModule {
|
||||
public static forRoot(): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: GuidedTourModule,
|
||||
providers: [ErrorHandler, GuidedTourService],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
import { debounceTime } from 'rxjs/operators';
|
||||
import { ErrorHandler, Inject, Injectable } from '@angular/core';
|
||||
import { Observable, Subject, fromEvent } from 'rxjs';
|
||||
import { GuidedTour, TourStep, Orientation, OrientationConfiguration } from './guided-tour.constants';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import { WindowRefService } from "./windowref.service";
|
||||
|
||||
@Injectable()
|
||||
export class GuidedTourService {
|
||||
public guidedTourCurrentStepStream: Observable<TourStep>;
|
||||
public guidedTourOrbShowingStream: Observable<boolean>;
|
||||
|
||||
private _guidedTourCurrentStepSubject = new Subject<TourStep>();
|
||||
private _guidedTourOrbShowingSubject = new Subject<boolean>();
|
||||
private _currentTourStepIndex = 0;
|
||||
private _currentTour: GuidedTour = null;
|
||||
private _onFirstStep = true;
|
||||
private _onLastStep = true;
|
||||
private _onResizeMessage = false;
|
||||
|
||||
constructor(
|
||||
public errorHandler: ErrorHandler,
|
||||
private windowRef: WindowRefService,
|
||||
@Inject(DOCUMENT) private dom
|
||||
) {
|
||||
this.guidedTourCurrentStepStream = this._guidedTourCurrentStepSubject.asObservable();
|
||||
this.guidedTourOrbShowingStream = this._guidedTourOrbShowingSubject.asObservable();
|
||||
|
||||
fromEvent(this.windowRef.nativeWindow, 'resize').pipe(debounceTime(200)).subscribe(() => {
|
||||
if (this._currentTour && this._currentTourStepIndex > -1) {
|
||||
if (this._currentTour.minimumScreenSize && this._currentTour.minimumScreenSize >= this.windowRef.nativeWindow.innerWidth) {
|
||||
this._onResizeMessage = true;
|
||||
const dialog = this._currentTour.resizeDialog || {
|
||||
title: 'Please resize',
|
||||
content: 'You have resized the tour to a size that is too small to continue. Please resize the browser to a larger size to continue the tour or close the tour.'
|
||||
};
|
||||
|
||||
this._guidedTourCurrentStepSubject.next(dialog);
|
||||
} else {
|
||||
this._onResizeMessage = false;
|
||||
this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public nextStep(): void {
|
||||
if (this._currentTour.steps[this._currentTourStepIndex].closeAction) {
|
||||
this._currentTour.steps[this._currentTourStepIndex].closeAction();
|
||||
}
|
||||
if (this._currentTour.steps[this._currentTourStepIndex + 1]) {
|
||||
this._currentTourStepIndex++;
|
||||
this._setFirstAndLast();
|
||||
if (this._currentTour.steps[this._currentTourStepIndex].action) {
|
||||
this._currentTour.steps[this._currentTourStepIndex].action();
|
||||
// Usually an action is opening something so we need to give it time to render.
|
||||
setTimeout(() => {
|
||||
if (this._checkSelectorValidity()) {
|
||||
this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex));
|
||||
} else {
|
||||
this.nextStep();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (this._checkSelectorValidity()) {
|
||||
this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex));
|
||||
} else {
|
||||
this.nextStep();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this._currentTour.completeCallback) {
|
||||
this._currentTour.completeCallback();
|
||||
}
|
||||
this.resetTour();
|
||||
}
|
||||
}
|
||||
|
||||
public backStep(): void {
|
||||
if (this._currentTour.steps[this._currentTourStepIndex].closeAction) {
|
||||
this._currentTour.steps[this._currentTourStepIndex].closeAction();
|
||||
}
|
||||
if (this._currentTour.steps[this._currentTourStepIndex - 1]) {
|
||||
this._currentTourStepIndex--;
|
||||
this._setFirstAndLast();
|
||||
if (this._currentTour.steps[this._currentTourStepIndex].action) {
|
||||
this._currentTour.steps[this._currentTourStepIndex].action();
|
||||
setTimeout(() => {
|
||||
if (this._checkSelectorValidity()) {
|
||||
this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex));
|
||||
} else {
|
||||
this.backStep();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (this._checkSelectorValidity()) {
|
||||
this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex));
|
||||
} else {
|
||||
this.backStep();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.resetTour();
|
||||
}
|
||||
}
|
||||
|
||||
public skipTour(): void {
|
||||
if (this._currentTour.skipCallback) {
|
||||
this._currentTour.skipCallback(this._currentTourStepIndex);
|
||||
}
|
||||
this.resetTour();
|
||||
}
|
||||
|
||||
public resetTour(): void {
|
||||
this.dom.body.classList.remove('tour-open');
|
||||
this._currentTour = null;
|
||||
this._currentTourStepIndex = 0;
|
||||
this._guidedTourCurrentStepSubject.next(null);
|
||||
}
|
||||
|
||||
public startTour(tour: GuidedTour): void {
|
||||
this._currentTour = cloneDeep(tour);
|
||||
this._currentTour.steps = this._currentTour.steps.filter(step => !step.skipStep);
|
||||
this._currentTourStepIndex = 0;
|
||||
this._setFirstAndLast();
|
||||
this._guidedTourOrbShowingSubject.next(this._currentTour.useOrb);
|
||||
if (
|
||||
this._currentTour.steps.length > 0
|
||||
&& (!this._currentTour.minimumScreenSize
|
||||
|| (this.windowRef.nativeWindow.innerWidth >= this._currentTour.minimumScreenSize))
|
||||
) {
|
||||
if (!this._currentTour.useOrb) {
|
||||
this.dom.body.classList.add('tour-open');
|
||||
}
|
||||
if (this._currentTour.steps[this._currentTourStepIndex].action) {
|
||||
this._currentTour.steps[this._currentTourStepIndex].action();
|
||||
}
|
||||
if (this._checkSelectorValidity()) {
|
||||
this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex));
|
||||
} else {
|
||||
this.nextStep();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public activateOrb(): void {
|
||||
this._guidedTourOrbShowingSubject.next(false);
|
||||
this.dom.body.classList.add('tour-open');
|
||||
}
|
||||
|
||||
private _setFirstAndLast(): void {
|
||||
this._onLastStep = (this._currentTour.steps.length - 1) === this._currentTourStepIndex;
|
||||
this._onFirstStep = this._currentTourStepIndex === 0;
|
||||
}
|
||||
|
||||
private _checkSelectorValidity(): boolean {
|
||||
if (this._currentTour.steps[this._currentTourStepIndex].selector) {
|
||||
const selectedElement = this.dom.querySelector(this._currentTour.steps[this._currentTourStepIndex].selector);
|
||||
if (!selectedElement) {
|
||||
this.errorHandler.handleError(
|
||||
// If error handler is configured this should not block the browser.
|
||||
new Error(`Error finding selector ${this._currentTour.steps[this._currentTourStepIndex].selector} on step ${this._currentTourStepIndex + 1} during guided tour: ${this._currentTour.tourId}`)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public get onLastStep(): boolean {
|
||||
return this._onLastStep;
|
||||
}
|
||||
|
||||
public get onFirstStep(): boolean {
|
||||
return this._onFirstStep;
|
||||
}
|
||||
|
||||
public get onResizeMessage(): boolean {
|
||||
return this._onResizeMessage;
|
||||
}
|
||||
|
||||
public get currentTourStepDisplay(): number {
|
||||
return this._currentTourStepIndex + 1;
|
||||
}
|
||||
|
||||
public get currentTourStepCount(): number {
|
||||
return this._currentTour && this._currentTour.steps ? this._currentTour.steps.length : 0;
|
||||
}
|
||||
|
||||
public get preventBackdropFromAdvancing(): boolean {
|
||||
return this._currentTour && this._currentTour.preventBackdropFromAdvancing;
|
||||
}
|
||||
|
||||
private getPreparedTourStep(index: number): TourStep {
|
||||
return this.setTourOrientation(this._currentTour.steps[index]);
|
||||
}
|
||||
|
||||
private setTourOrientation(step: TourStep): TourStep {
|
||||
const convertedStep = cloneDeep(step);
|
||||
if (
|
||||
convertedStep.orientation
|
||||
&& !(typeof convertedStep.orientation === 'string')
|
||||
&& (convertedStep.orientation as OrientationConfiguration[]).length
|
||||
) {
|
||||
(convertedStep.orientation as OrientationConfiguration[]).sort((a: OrientationConfiguration, b: OrientationConfiguration) => {
|
||||
if (!b.maximumSize) {
|
||||
return 1;
|
||||
}
|
||||
if (!a.maximumSize) {
|
||||
return -1;
|
||||
}
|
||||
return b.maximumSize - a.maximumSize;
|
||||
});
|
||||
|
||||
let currentOrientation: Orientation = Orientation.Top;
|
||||
(convertedStep.orientation as OrientationConfiguration[]).forEach(
|
||||
(orientationConfig: OrientationConfiguration) => {
|
||||
if (!orientationConfig.maximumSize || this.windowRef.nativeWindow.innerWidth <= orientationConfig.maximumSize) {
|
||||
currentOrientation = orientationConfig.orientationDirection;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
convertedStep.orientation = currentOrientation;
|
||||
}
|
||||
return convertedStep;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { Inject, Injectable, PLATFORM_ID } from "@angular/core";
|
||||
import { isPlatformBrowser } from "@angular/common";
|
||||
|
||||
function getWindow(): any {
|
||||
return window;
|
||||
}
|
||||
|
||||
function getMockWindow(): any {
|
||||
return {
|
||||
innerWidth: 0,
|
||||
innerHeight: 0,
|
||||
scrollY: 0,
|
||||
scrollX: 0,
|
||||
pageYOffset: 0,
|
||||
pageXOffset: 0,
|
||||
scroll: () => {},
|
||||
scrollTo: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WindowRefService {
|
||||
private readonly isBrowser: boolean = false;
|
||||
|
||||
get nativeWindow(): any {
|
||||
if (this.isBrowser) {
|
||||
return getWindow();
|
||||
} else {
|
||||
return getMockWindow();
|
||||
}
|
||||
}
|
||||
|
||||
constructor(@Inject(PLATFORM_ID) platformId) {
|
||||
this.isBrowser = isPlatformBrowser(platformId);
|
||||
}
|
||||
}
|
|
@ -34,6 +34,11 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- End of Sort by -->
|
||||
<!-- Guided Tour -->
|
||||
<div class="center-content" [style.display]=" (!isVisible && isAuthenticated()) ? 'block' : 'none'" (click)="restartTour()">
|
||||
{{ 'GENERAL.ACTIONS.TAKE-A-TOUR'| translate }}
|
||||
</div>
|
||||
<!-- End of Guided Tour -->
|
||||
<!-- Search Filter-->
|
||||
<mat-form-field appearance="outline" class="search-form ml-auto col-auto" floatLabel="never">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
|
|
|
@ -137,6 +137,19 @@
|
|||
height: 45px;
|
||||
}
|
||||
|
||||
.center-content {
|
||||
width: 100%;
|
||||
min-width: 10rem;
|
||||
margin: auto;
|
||||
padding: 0 15px;
|
||||
text-align: right;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02rem;
|
||||
color: #2D72D6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
// font-size: 12px;
|
||||
text-align: left;
|
||||
|
|
|
@ -24,6 +24,8 @@ import { MatDialog } from '@angular/material';
|
|||
import { FormGroup, FormBuilder, FormControl } from '@angular/forms';
|
||||
import { RecentActivityOrder } from '@app/core/common/enum/recent-activity-order';
|
||||
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
|
||||
import { GuidedTourService } from '@app/library/guided-tour/guided-tour.service';
|
||||
import { GuidedTour, Orientation } from '@app/library/guided-tour/guided-tour.constants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dataset-listing-component',
|
||||
|
@ -59,6 +61,8 @@ export class DatasetListingComponent extends BaseComponent implements OnInit, IB
|
|||
|
||||
scrollbar: boolean;
|
||||
order = RecentActivityOrder;
|
||||
dmpText: string;
|
||||
datasetText: string;
|
||||
|
||||
constructor(
|
||||
private datasetService: DatasetService,
|
||||
|
@ -68,7 +72,9 @@ export class DatasetListingComponent extends BaseComponent implements OnInit, IB
|
|||
private dmpService: DmpService,
|
||||
private language: TranslateService,
|
||||
private authService: AuthService,
|
||||
public enumUtils: EnumUtils
|
||||
public enumUtils: EnumUtils,
|
||||
private authentication: AuthService,
|
||||
private guidedTourService: GuidedTourService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
@ -133,6 +139,27 @@ export class DatasetListingComponent extends BaseComponent implements OnInit, IB
|
|||
this.scrollbar = this.hasScrollbar();
|
||||
}
|
||||
|
||||
public dashboardTour: GuidedTour = {
|
||||
tourId: 'dmp-dataset-tour',
|
||||
useOrb: true,
|
||||
steps: [
|
||||
{
|
||||
selector: '.dmp-tour',
|
||||
content: 'Step 1',
|
||||
orientation: Orientation.Right
|
||||
},
|
||||
{
|
||||
selector: '.dataset-tour',
|
||||
content: 'Step 2',
|
||||
orientation: Orientation.Right
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
public isAuthenticated(): boolean {
|
||||
return !(!this.authentication.current());
|
||||
}
|
||||
|
||||
controlModified(): void {
|
||||
// this.clearErrorModel();
|
||||
// if (this.refreshCallback != null &&
|
||||
|
@ -327,6 +354,28 @@ export class DatasetListingComponent extends BaseComponent implements OnInit, IB
|
|||
return merged;
|
||||
}
|
||||
|
||||
public setDashboardTourDmpText(): void {
|
||||
this.dmpText = this.language.instant('DMP-LISTING.TEXT-INFO') + '\n\n' +
|
||||
this.language.instant('DMP-LISTING.TEXT-INFO-QUESTION') + ' ' +
|
||||
this.language.instant('DMP-LISTING.LINK-ZENODO') + ' ' +
|
||||
this.language.instant('DMP-LISTING.GET-IDEA');
|
||||
this.dashboardTour.steps[0].title = this.dmpText;
|
||||
}
|
||||
|
||||
public setDashboardTourDatasetText(): void {
|
||||
this.datasetText = this.language.instant('DATASET-LISTING.TEXT-INFO') +
|
||||
this.language.instant('DATASET-LISTING.LINK-PUBLIC-DATASETS') + ' ' +
|
||||
this.language.instant('DATASET-LISTING.TEXT-INFO-REST') + '\n\n' +
|
||||
this.language.instant('DATASET-LISTING.TEXT-INFO-PAR');
|
||||
this.dashboardTour.steps[1].title = this.datasetText;
|
||||
}
|
||||
|
||||
public restartTour(): void {
|
||||
this.setDashboardTourDmpText();
|
||||
this.setDashboardTourDatasetText();
|
||||
this.guidedTourService.startTour(this.dashboardTour);
|
||||
}
|
||||
|
||||
// rowClicked(dataset: DatasetListingModel) {
|
||||
// this.router.navigate(['/datasets/edit/' + dataset.id]);
|
||||
// }
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- End of Sort by -->
|
||||
<!-- Guided Tour -->
|
||||
<div class="center-content" [style.display]="!isVisible && isAuthenticated()? 'block' : 'none'" (click)="restartTour()">
|
||||
{{ 'GENERAL.ACTIONS.TAKE-A-TOUR'| translate }}
|
||||
</div>
|
||||
<!-- End of Guided Tour -->
|
||||
<!-- Search Filter-->
|
||||
<mat-form-field appearance="outline" class="search-form ml-auto col-auto pr-0" floatLabel="never">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
|
|
|
@ -241,6 +241,19 @@
|
|||
padding: 0.3rem 0rem 0.6rem 0rem !important;
|
||||
}
|
||||
|
||||
.center-content {
|
||||
width: 100%;
|
||||
min-width: 10rem;
|
||||
margin: auto;
|
||||
padding: 0 15px;
|
||||
text-align: right;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02rem;
|
||||
color: #2D72D6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// .bot-paginator {
|
||||
// margin-top: auto;
|
||||
// }
|
||||
|
|
|
@ -26,7 +26,8 @@ import { AuthService } from '@app/core/services/auth/auth.service';
|
|||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { DmpCriteriaDialogComponent } from './criteria/dmp-criteria-dialog.component';
|
||||
import { RecentActivityOrder } from '@app/core/common/enum/recent-activity-order';
|
||||
|
||||
import { GuidedTourService } from '@app/library/guided-tour/guided-tour.service';
|
||||
import { GuidedTour, Orientation } from '@app/library/guided-tour/guided-tour.constants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dmp-listing-component',
|
||||
|
@ -63,6 +64,8 @@ export class DmpListingComponent extends BaseComponent implements OnInit, IBread
|
|||
|
||||
scrollbar: boolean;
|
||||
order = RecentActivityOrder;
|
||||
dmpText: string;
|
||||
datasetText: string;
|
||||
|
||||
constructor(
|
||||
private dmpService: DmpService,
|
||||
|
@ -73,7 +76,8 @@ export class DmpListingComponent extends BaseComponent implements OnInit, IBread
|
|||
private language: TranslateService,
|
||||
private grantService: GrantService,
|
||||
private uiNotificationService: UiNotificationService,
|
||||
private authService: AuthService
|
||||
private authService: AuthService,
|
||||
private guidedTourService: GuidedTourService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
@ -173,6 +177,26 @@ export class DmpListingComponent extends BaseComponent implements OnInit, IBread
|
|||
.subscribe(x => this.refresh());
|
||||
}
|
||||
|
||||
public dashboardTour: GuidedTour = {
|
||||
tourId: 'dmp-dataset-tour',
|
||||
useOrb: true,
|
||||
steps: [
|
||||
{
|
||||
selector: '.dmp-tour',
|
||||
content: 'Step 1',
|
||||
orientation: Orientation.Right
|
||||
},
|
||||
{
|
||||
selector: '.dataset-tour',
|
||||
content: 'Step 2',
|
||||
orientation: Orientation.Right
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
public isAuthenticated(): boolean {
|
||||
return !(!this.authService.current());
|
||||
}
|
||||
ngAfterContentChecked(): void {
|
||||
this.scrollbar = this.hasScrollbar();
|
||||
}
|
||||
|
@ -426,6 +450,28 @@ export class DmpListingComponent extends BaseComponent implements OnInit, IBread
|
|||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
public setDashboardTourDmpText(): void {
|
||||
this.dmpText = this.language.instant('DMP-LISTING.TEXT-INFO') + '\n\n' +
|
||||
this.language.instant('DMP-LISTING.TEXT-INFO-QUESTION') + ' ' +
|
||||
this.language.instant('DMP-LISTING.LINK-ZENODO') + ' ' +
|
||||
this.language.instant('DMP-LISTING.GET-IDEA');
|
||||
this.dashboardTour.steps[0].title = this.dmpText;
|
||||
}
|
||||
|
||||
public setDashboardTourDatasetText(): void {
|
||||
this.datasetText = this.language.instant('DATASET-LISTING.TEXT-INFO') +
|
||||
this.language.instant('DATASET-LISTING.LINK-PUBLIC-DATASETS') + ' ' +
|
||||
this.language.instant('DATASET-LISTING.TEXT-INFO-REST') + '\n\n' +
|
||||
this.language.instant('DATASET-LISTING.TEXT-INFO-PAR');
|
||||
this.dashboardTour.steps[1].title = this.datasetText;
|
||||
}
|
||||
|
||||
public restartTour(): void {
|
||||
this.setDashboardTourDmpText();
|
||||
this.setDashboardTourDatasetText();
|
||||
this.guidedTourService.startTour(this.dashboardTour);
|
||||
}
|
||||
}
|
||||
|
||||
// export class DmpDataSource extends DataSource<DmpListingModel> {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<hr *ngIf="!firstGroup">
|
||||
<mat-list-item routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" *ngFor="let groupMenuRoute of groupMenuItem.routes; let first = first" class="nav-item"
|
||||
[ngClass]="{'mt-4': first && firstGroup}">
|
||||
<a class="nav-link nav-row" [routerLink]="[groupMenuRoute.path]">
|
||||
<a class="nav-link nav-row" [routerLink]="[groupMenuRoute.path]" [ngClass]="{'dmp-tour': groupMenuRoute.path == '/plans', 'dataset-tour' : groupMenuRoute.path == '/datasets'}">
|
||||
<i class="material-icons icon">{{ groupMenuRoute.icon }}</i>
|
||||
<i *ngIf="groupMenuRoute.path == '/plans' || groupMenuRoute.path == '/datasets'" class="material-icons icon-mask">person</i>
|
||||
<span [ngClass]="{'pl-0': groupMenuRoute.path == '/plans' || groupMenuRoute.path == '/datasets'}">{{groupMenuRoute.title | translate}}</span>
|
||||
|
|
|
@ -79,7 +79,8 @@
|
|||
"VIEW-ALL": "Alles anzeigen",
|
||||
"SHOW-MORE": "Mehr anzeigen",
|
||||
"SHOW-LESS": "Weniger anzeigen",
|
||||
"LOG-IN": "Einloggen"
|
||||
"LOG-IN": "Einloggen",
|
||||
"TAKE-A-TOUR": "Do you need help? Take a tour.."
|
||||
},
|
||||
"PREPOSITIONS": {
|
||||
"OF": "von"
|
||||
|
|
|
@ -81,7 +81,8 @@
|
|||
"SHOW-MORE": "Show more",
|
||||
"LOAD-MORE": "Load more",
|
||||
"SHOW-LESS": "Show less",
|
||||
"LOG-IN": "Log in"
|
||||
"LOG-IN": "Log in",
|
||||
"TAKE-A-TOUR": "Do you need help? Take a tour.."
|
||||
},
|
||||
"PREPOSITIONS": {
|
||||
"OF": "of"
|
||||
|
|
|
@ -80,7 +80,8 @@
|
|||
"VIEW-ALL": "Ver todo",
|
||||
"SHOW-MORE": "Mostrar más",
|
||||
"SHOW-LESS": "Show less",
|
||||
"LOG-IN": "Iniciar sesión"
|
||||
"LOG-IN": "Iniciar sesión",
|
||||
"TAKE-A-TOUR": "Do you need help? Take a tour.."
|
||||
},
|
||||
"PREPOSITIONS": {
|
||||
"OF": "de"
|
||||
|
|
|
@ -80,7 +80,8 @@
|
|||
"VIEW-ALL": "Προβολή όλων",
|
||||
"SHOW-MORE": "Δείτε περισσότερα",
|
||||
"SHOW-LESS": "Δείτε λιγότερα",
|
||||
"LOG-IN": "Σύνδεση"
|
||||
"LOG-IN": "Σύνδεση",
|
||||
"TAKE-A-TOUR": "Do you need help? Take a tour.."
|
||||
},
|
||||
"PREPOSITIONS": {
|
||||
"OF": "of"
|
||||
|
@ -1239,9 +1240,7 @@
|
|||
"SEARCH": "ΑΝΑΖΗΤΗΣΗ...",
|
||||
"DATA-MANAGEMENT-PLANS": "ΣΧΕΔΙΑ ΔΙΑΧΕΙΡΙΣΗΣ ΔΕΔΟΜΕΝΩΝ",
|
||||
"PERSONAL-USAGE": "Προσωπική Χρήση",
|
||||
"DMPS": "DMPs",
|
||||
"DATASET-DESCRIPTIONS": "Περιγραφές Dataset",
|
||||
"GRANTS": "Grants",
|
||||
"RELATED-ORGANISATIONS": "Σχετικοί Οργανισμοί",
|
||||
"DRAFTS": "Προσχέδια",
|
||||
"ALL": "Όλα",
|
||||
|
|
|
@ -79,7 +79,8 @@
|
|||
"VIEW-ALL": "Tümüne Gör",
|
||||
"SHOW-MORE": "Daha fazla göster",
|
||||
"SHOW-LESS": "Daha az göster",
|
||||
"LOG-IN": "Oturum aç"
|
||||
"LOG-IN": "Oturum aç",
|
||||
"TAKE-A-TOUR": "Do you need help? Take a tour.."
|
||||
},
|
||||
"PREPOSITIONS": {
|
||||
"OF": "nın"
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
Loading…
Reference in New Issue