From 5e5106b722e93b435487b0186bf34189ce1532d0 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Thu, 8 Apr 2021 14:42:39 +0200 Subject: [PATCH] feat: rework tour component --- common/constants.js | 2 +- .../atlasLayerSelector.component.ts | 23 +- .../atlasLayerSelector.template.html | 19 +- src/ui/quickTour/arrowCmp/arrow.component.ts | 66 ++++ src/ui/quickTour/arrowCmp/arrow.style.css | 48 +++ src/ui/quickTour/arrowCmp/arrow.template.html | 57 +++ src/ui/quickTour/constrants.ts | 33 +- src/ui/quickTour/module.ts | 14 +- src/ui/quickTour/quickTour.directive.ts | 54 +-- src/ui/quickTour/quickTour.service.ts | 113 ++++-- .../quickTourComponent/quickTour.component.ts | 324 ++++++++++++++++++ .../quickTourComponent/quickTour.style.css | 23 ++ .../quickTour.template.html | 43 +++ src/ui/quickTour/quickTourThis.directive.ts | 51 +-- .../quickToutComponent/quickTour.component.ts | 212 ------------ .../quickToutComponent/quickTour.style.css | 77 ----- .../quickToutComponent/quickTour.temlate.html | 118 ------- .../topMenu/topMenuCmp/topMenu.components.ts | 8 +- .../topMenu/topMenuCmp/topMenu.template.html | 9 +- src/util/LinkedList.ts | 94 +++++ src/util/index.ts | 5 + src/util/windowResize/index.ts | 2 + src/util/windowResize/module.ts | 17 + .../windowResize/windowResize.directive.ts | 64 ++++ src/util/windowResize/windowResize.service.ts | 30 ++ src/viewerModule/nehuba/module.ts | 2 + .../nehubaViewerGlue.component.ts | 41 ++- .../nehubaViewerGlue.template.html | 50 ++- .../nehuba/statusCard/statusCard.component.ts | 11 +- .../statusCard/statusCard.template.html | 184 +++++----- .../viewerCmp/viewerCmp.component.ts | 19 +- .../viewerCmp/viewerCmp.template.html | 24 +- 32 files changed, 1119 insertions(+), 718 deletions(-) create mode 100644 src/ui/quickTour/arrowCmp/arrow.component.ts create mode 100644 src/ui/quickTour/arrowCmp/arrow.style.css create mode 100644 src/ui/quickTour/arrowCmp/arrow.template.html create mode 100644 src/ui/quickTour/quickTourComponent/quickTour.component.ts create mode 100644 src/ui/quickTour/quickTourComponent/quickTour.style.css create mode 100644 src/ui/quickTour/quickTourComponent/quickTour.template.html delete mode 100644 src/ui/quickTour/quickToutComponent/quickTour.component.ts delete mode 100644 src/ui/quickTour/quickToutComponent/quickTour.style.css delete mode 100644 src/ui/quickTour/quickToutComponent/quickTour.temlate.html create mode 100644 src/util/LinkedList.ts create mode 100644 src/util/windowResize/index.ts create mode 100644 src/util/windowResize/module.ts create mode 100644 src/util/windowResize/windowResize.directive.ts create mode 100644 src/util/windowResize/windowResize.service.ts diff --git a/common/constants.js b/common/constants.js index d827b1361..97fcd1559 100644 --- a/common/constants.js +++ b/common/constants.js @@ -83,7 +83,7 @@ exports.QUICKTOUR_DESC ={ REGION_SEARCH: `Use the region quick search for finding, selecting and navigating brain regions in the selected parcellation map.`, ATLAS_SELECTOR: `This is the atlas selector. Click here to choose between EBRAINS reference atlases of different species.`, - CHIPS: `These „chips“ indicate the currently selected parcellation map as well as selected region. Click the chip to see different versions, if any. Click (i) to read more about a selected item. Click (x) to clear a selection.`, + CHIPS: `These "chips" indicate the currently selected parcellation map as well as selected region. Click the chip to see different versions, if any. Click (i) to read more about a selected item. Click (x) to clear a selection.`, SLICE_VIEW: `The planar views allow you to zoom in to full resolution (mouse wheel), pan the view (click+drag), and select oblique sections (shift+click+drag). You can double-click brain regions to select them.`, PERSPECTIVE_VIEW: `The 3D view gives an overview of the brain with limited resolution. It can be independently rotated. Click the „eye“ icon on the bottom left to toggle pure surface view.`, VIEW_ICONS: `Use these icons in any of the views to maximize it and zoom in/out.`, diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts index eccfa00f6..86aaff323 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts @@ -1,12 +1,12 @@ -import {Component, OnInit, ViewChildren, QueryList, HostBinding, ElementRef, ViewChild} from "@angular/core"; +import {Component, OnInit, ViewChildren, QueryList, HostBinding } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { distinctUntilChanged, map, withLatestFrom, shareReplay, groupBy, mergeMap, toArray, switchMap, scan, filter } from "rxjs/operators"; -import {Observable, Subscription, from, zip, of, combineLatest, BehaviorSubject} from "rxjs"; +import {Observable, Subscription, from, zip, of, combineLatest } from "rxjs"; import { viewerStateSelectTemplateWithId, viewerStateToggleLayer } from "src/services/state/viewerState.store.helper"; import { MatMenuTrigger } from "@angular/material/menu"; import { viewerStateGetSelectedAtlas, viewerStateAtlasLatestParcellationSelector, viewerStateSelectedTemplateFullInfoSelector, viewerStateSelectedTemplatePureSelector, viewerStateSelectedParcellationSelector } from "src/services/state/viewerState/selectors"; import { ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' -import {QuickTourData, QuickTourPosition} from "src/ui/quickTour/constrants"; +import { IQuickTourData } from "src/ui/quickTour/constrants"; @Component({ selector: 'atlas-layer-selector', @@ -91,16 +91,10 @@ export class AtlasLayerSelector implements OnInit { @HostBinding('attr.data-opened') public selectorExpanded: boolean = false public selectedTemplatePreviewUrl: string = '' - - @ViewChild('expandedSelectorCard', {read: ElementRef}) expandedSelectorCard: ElementRef<HTMLElement> - quickTourCollapsedPosition: QuickTourPosition = {position: 'top-right', arrow: 'arrow2', arrowPosition: 'left', arrowAlign: 'bottom', arrowMargin: {left: -20}} - quickTourExpandedPosition: QuickTourPosition = {position: 'right', arrow: 'arrow5', arrowPosition: 'left', arrowAlign: 'center', - top: document.body.offsetHeight - this.expandedSelectorCard?.nativeElement.offsetHeight/2, - left: this.expandedSelectorCard?.nativeElement.offsetWidth} - public quickTourData: QuickTourData = { + + public quickTourData: IQuickTourData = { order: 4, description: QUICKTOUR_DESC.LAYER_SELECTOR, - position: this.quickTourCollapsedPosition } public availableTemplates$ = this.store$.pipe<any[]>( @@ -158,13 +152,6 @@ export class AtlasLayerSelector implements OnInit { toggleSelector() { this.selectorExpanded = !this.selectorExpanded - if (this.selectorExpanded) { - this.quickTourExpandedPosition.left = this.expandedSelectorCard?.nativeElement.offsetWidth - this.quickTourExpandedPosition.top = document.body.offsetHeight - this.expandedSelectorCard?.nativeElement.offsetHeight/2 - this.quickTourData.position = this.quickTourExpandedPosition - } else { - this.quickTourData.position = this.quickTourCollapsedPosition - } } selectTemplateWithName(template) { diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html index da8913482..2676a48d5 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html @@ -2,7 +2,7 @@ <!-- selector panel when expanded --> - <mat-card #expandedSelectorCard class="selector-container position-absolute" [ngClass]="selectorExpanded ? 'scale-up-bl pe-all' : 'scale-down-bl pe-none'"> + <mat-card class="selector-container position-absolute" [ngClass]="selectorExpanded ? 'scale-up-bl pe-all' : 'scale-down-bl pe-none'"> <mat-card-content> <!-- templates --> @@ -64,16 +64,15 @@ <!-- place holder when not expanded --> <div class="position-relative m-2 cursor-pointer scale-up-bl pe-all" - quick-tour - [quick-tour-description]="quickTourData.description" - [quick-tour-order]="quickTourData.order" - [quick-tour-position]="quickTourData.position"> + quick-tour + [quick-tour-description]="quickTourData.description" + [quick-tour-order]="quickTourData.order"> <button color="primary" - matTooltip="Select layer" - mat-mini-fab - *ngIf="shouldShowRenderPlaceHolder$ | async" - [attr.aria-label]="TOGGLE_ATLAS_LAYER_SELECTOR" - (click)="toggleSelector()"> + matTooltip="Select layer" + mat-mini-fab + *ngIf="shouldShowRenderPlaceHolder$ | async" + [attr.aria-label]="TOGGLE_ATLAS_LAYER_SELECTOR" + (click)="toggleSelector()"> <i class="fas fa-layer-group"></i> </button> </div> diff --git a/src/ui/quickTour/arrowCmp/arrow.component.ts b/src/ui/quickTour/arrowCmp/arrow.component.ts new file mode 100644 index 000000000..00eeb1806 --- /dev/null +++ b/src/ui/quickTour/arrowCmp/arrow.component.ts @@ -0,0 +1,66 @@ +import { Component, HostBinding, Input, OnChanges } from "@angular/core"; + +@Component({ + selector: 'quick-tour-arrow', + templateUrl: './arrow.template.html', + styleUrls: [ + './arrow.style.css' + ] +}) + +export class ArrowComponent implements OnChanges{ + + @HostBinding('style.transform') + transform = `translate(0px, 0px)` + + stemStyle = {} + + headTranslate = 'translate(0px, 0px)' + + headStyle = { + transform: `rotate(0deg)` + } + + @Input('quick-tour-arrow-from') + fromPos: [number, number] + + @Input('quick-tour-arrow-to') + toPos: [number, number] + + @Input('quick-tour-arrow-type') + type: 'straight' | 'concave-from-top' | 'concave-from-bottom' = 'straight' + + ngOnChanges(){ + let rotate: string + switch(this.type) { + case 'concave-from-top': { + rotate = '0deg' + break + } + case 'concave-from-bottom': { + rotate = '180deg' + break + } + default: { + rotate = `${(Math.PI / 2) + Math.atan2( + (this.toPos[1] - this.fromPos[1]), + (this.toPos[0] - this.fromPos[0]) + )}rad` + } + } + + this.transform = `translate(${this.fromPos[0]}px, ${this.fromPos[1]}px)` + + this.headTranslate = ` + translateX(-2rem) + translate(${this.toPos[0] - this.fromPos[0]}px, ${this.toPos[1] - this.fromPos[1]}px) + rotate(${rotate}) + ` + const x = (this.toPos[0] - this.fromPos[0]) / 100 + const y = (this.toPos[1] - this.fromPos[1]) / 100 + + this.stemStyle = { + transform: `scale(${x}, ${y})` + } + } +} diff --git a/src/ui/quickTour/arrowCmp/arrow.style.css b/src/ui/quickTour/arrowCmp/arrow.style.css new file mode 100644 index 000000000..f4355c89c --- /dev/null +++ b/src/ui/quickTour/arrowCmp/arrow.style.css @@ -0,0 +1,48 @@ +:host +{ + width: 0px; + height: 0px; +} + +:host-context([darktheme="true"]) svg +{ + stroke: rgb(200, 200, 200); +} + +svg +{ + pointer-events: none; + stroke-width: 5px; + stroke: rgb(255, 255, 255); + stroke-linecap: round; + stroke-linejoin: round; +} + +/* https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/vector-effect */ +svg path +{ + vector-effect: non-scaling-stroke; +} + +#arrow_head +{ + width: 4rem; + height: 4rem; + transform-origin: 50% 0; +} + +#arrow_head #arrow_head_group +{ + transform-origin: 50% 10%; + transform: scale(0.95, 0.80); +} + +#arrow_stem +{ + transform-origin: 0 0; + width: 100px; + height: 100px; + position: absolute; + left: 0; + top: 0; +} diff --git a/src/ui/quickTour/arrowCmp/arrow.template.html b/src/ui/quickTour/arrowCmp/arrow.template.html new file mode 100644 index 000000000..39281bdff --- /dev/null +++ b/src/ui/quickTour/arrowCmp/arrow.template.html @@ -0,0 +1,57 @@ +<!-- arrow head point to top --> + +<svg id="arrow_head" + [style.transform]="headTranslate" + viewBox="0 0 100 100" + fill="none" + xmlns="http://www.w3.org/2000/svg"> + <g id="arrow_head_group"> + <path id="arrrow_head_path" d="M1 53C1 53 9.5 49 18.5 40.5C22.4557 36.7641 34.4615 25.5 41 17C46 10.5 52 1.5 52 1.5C52 1.5 59.5 7 68 15M68 15C72.3675 19.1106 82.5632 29.8269 89.5 38.5M68 15L89.5 38.5M99.5 53C99.5 53 96 47.5 89.5 38.5"/> + </g> +</svg> + +<!-- stem --> +<ng-container [ngSwitch]="type"> + <!-- arrow stem, bottom left top right, concave from top left --> + <ng-template [ngSwitchCase]="'concave-from-top'"> + <svg id="arrow_stem" + [style]="stemStyle" + viewBox="0 0 100 100" + fill="none" + preserveAspectRatio="none" + xmlns="http://www.w3.org/2000/svg"> + <g> + <path id="arrow_stem_concave_2" d="M0 0.499998C0 0.499998 26.0937 -0.22897 42 4C56.0254 7.72889 65.0753 9.44672 76 19C89.5421 30.8421 92.3854 42.6124 97 60C100.968 74.9525 100 99.5 100 99.5" /> + </g> + </svg> + + </ng-template> + <ng-template [ngSwitchCase]="'concave-from-bottom'"> + <svg id="arrow_stem" + [style]="stemStyle" + viewBox="0 0 100 100" + fill="none" + preserveAspectRatio="none" + xmlns="http://www.w3.org/2000/svg"> + <g> + <path id="arrow_stem_concave_2" d="M0 0.499998C0 0.499998 26.0937 -0.22897 42 4C56.0254 7.72889 65.0753 9.44672 76 19C89.5421 30.8421 92.3854 42.6124 97 60C100.968 74.9525 100 99.5 100 99.5" /> + </g> + </svg> + + </ng-template> + + <!-- arrow stem, straight --> + <ng-template [ngSwitchDefault]> + <svg id="arrow_stem" + [style]="stemStyle" + viewBox="0 0 100 100" + fill="none" + preserveAspectRatio="none" + xmlns="http://www.w3.org/2000/svg"> + + <g id="arrow_stem_straight"> + <path id="arrow_stem_straight_path" d="M0 0 100 100" /> + </g> + </svg> + </ng-template> +</ng-container> diff --git a/src/ui/quickTour/constrants.ts b/src/ui/quickTour/constrants.ts index 2e4b8c2f3..45e986f66 100644 --- a/src/ui/quickTour/constrants.ts +++ b/src/ui/quickTour/constrants.ts @@ -1,20 +1,23 @@ -export interface QuickTourData { +import { TemplateRef } from "@angular/core" + +type TPosition = 'top' | 'top-right' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' | 'top-left' + +type TCustomPosition = { + left: number + top: number +} + +export interface IQuickTourData { order: number description: string - position?: QuickTourPosition + tourPosition?: TPosition + overwritePosition?: IQuickTourOverwritePosition + overwriteArrow?: TemplateRef<any> | string } -export interface QuickTourPosition { - // Position of tip - position?: 'top' | 'top-right' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' | 'top-left' - align?: 'right' | 'left' | 'top' | 'bottom' | 'center' - left?: number - top?: number - - // Position of arrow - arrow?: string - arrowPosition?: 'top' | 'top-right' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' | 'top-left' - arrowAlign?: 'right' | 'left' | 'top' | 'bottom' | 'center' - arrowMargin?: {top?: number, right?: number, bottom?: number, left?: number} - arrowTransform?: string +export interface IQuickTourOverwritePosition { + dialog: TCustomPosition + arrow: TCustomPosition } + +export type TQuickTourPosition = TPosition diff --git a/src/ui/quickTour/module.ts b/src/ui/quickTour/module.ts index d34eb0bb2..f7f0d4c08 100644 --- a/src/ui/quickTour/module.ts +++ b/src/ui/quickTour/module.ts @@ -3,21 +3,25 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { UtilModule } from "src/util"; import { AngularMaterialModule } from "../sharedModules/angularMaterial.module"; -import {QuickTourThis} from "src/ui/quickTour/quickTourThis.directive"; -import {QuickTourService} from "src/ui/quickTour/quickTour.service"; -import {QuickTourComponent} from "src/ui/quickTour/quickToutComponent/quickTour.component"; -import {QuickTourDirective} from "src/ui/quickTour/quickTour.directive"; +import { QuickTourThis } from "src/ui/quickTour/quickTourThis.directive"; +import { QuickTourService } from "src/ui/quickTour/quickTour.service"; +import { QuickTourComponent } from "src/ui/quickTour/quickTourComponent/quickTour.component"; +import { QuickTourDirective } from "src/ui/quickTour/quickTour.directive"; +import { ArrowComponent } from "./arrowCmp/arrow.component"; +import { WindowResizeModule } from "src/util/windowResize"; @NgModule({ imports: [ CommonModule, AngularMaterialModule, UtilModule, + WindowResizeModule, ], declarations:[ QuickTourThis, QuickTourComponent, - QuickTourDirective + QuickTourDirective, + ArrowComponent, ], exports: [ QuickTourDirective, diff --git a/src/ui/quickTour/quickTour.directive.ts b/src/ui/quickTour/quickTour.directive.ts index 318c8d0c2..099084674 100644 --- a/src/ui/quickTour/quickTour.directive.ts +++ b/src/ui/quickTour/quickTour.directive.ts @@ -1,9 +1,5 @@ -import { Overlay, OverlayRef } from "@angular/cdk/overlay"; -import { ComponentPortal } from "@angular/cdk/portal"; -import {ComponentRef, Directive, HostListener} from "@angular/core"; -import { take } from "rxjs/operators"; -import {QuickTourService} from "src/ui/quickTour/quickTour.service"; -import {QuickTourComponent} from "src/ui/quickTour/quickToutComponent/quickTour.component"; +import { Directive, HostListener } from "@angular/core"; +import { QuickTourService } from "./quickTour.service"; @Directive({ selector: '[quick-tour-opener]' @@ -11,57 +7,19 @@ import {QuickTourComponent} from "src/ui/quickTour/quickToutComponent/quickTour. export class QuickTourDirective { - constructor(private overlay: Overlay, - private quickTourService: QuickTourService){} - - public touring = false - - private overlayRef: OverlayRef - private cmpRef: ComponentRef<QuickTourComponent> + constructor( + private quickTourService: QuickTourService + ){} @HostListener('window:keydown', ['$event']) keyListener(ev: KeyboardEvent){ if (ev.key === 'Escape') { - if (this.overlayRef) this.dispose() + this.quickTourService.endTour() } } @HostListener('click') onClick(){ - if (this.overlayRef) this.dispose() - this.overlayRef = this.overlay.create({ - height: '0px', - width: '0px', - hasBackdrop: false, - positionStrategy: this.overlay.position().global(), - }) - - this.cmpRef = this.overlayRef.attach( - new ComponentPortal(QuickTourComponent) - ) - - this.cmpRef.instance.destroy.pipe( - take(1) - ).subscribe( - () => { - this.dispose() - } - ) - this.quickTourService.startTour() } - - dispose(){ - this.cmpRef = null - if (this.overlayRef) this.overlayRef.dispose() - this.overlayRef = null - } - - reset(){ - this.touring = true - } - - clear(){ - this.touring = false - } } diff --git a/src/ui/quickTour/quickTour.service.ts b/src/ui/quickTour/quickTour.service.ts index db3460a59..9b9cd0739 100644 --- a/src/ui/quickTour/quickTour.service.ts +++ b/src/ui/quickTour/quickTour.service.ts @@ -1,60 +1,103 @@ -import {Injectable} from "@angular/core"; -import {QuickTourThis} from "src/ui/quickTour/quickTourThis.directive"; -import {BehaviorSubject} from "rxjs"; +import { ComponentRef, Injectable } from "@angular/core"; +import { BehaviorSubject, Subject } from "rxjs"; +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { ComponentPortal } from "@angular/cdk/portal"; +import { QuickTourComponent } from "./quickTourComponent/quickTour.component"; +import { QuickTourThis } from "./quickTourThis.directive"; +import { DoublyLinkedList, IDoublyLinkedItem } from 'src/util' + +export function findInLinkedList<T>(first: IDoublyLinkedItem<T>, predicate: (linkedObj: IDoublyLinkedItem<T>) => boolean): IDoublyLinkedItem<T>{ + let compareObj = first, + returnObj: IDoublyLinkedItem<T> = null + + do { + if (predicate(compareObj)) { + returnObj = compareObj + break + } + compareObj = compareObj.next + } while(!!compareObj) + + return returnObj +} @Injectable() export class QuickTourService { + private overlayRef: OverlayRef + private cmpRef: ComponentRef<QuickTourComponent> + public currSlideNum: number = null - public currentTip$: BehaviorSubject<any> = new BehaviorSubject(null) + public currentTip$: BehaviorSubject<IDoublyLinkedItem<QuickTourThis>> = new BehaviorSubject(null) + public detectChanges$: Subject<null> = new Subject() - public quickTourThisDirectives: QuickTourThis[] = [] + public currActiveSlide: IDoublyLinkedItem<QuickTourThis> + public slides = new DoublyLinkedList<QuickTourThis>() - public register(dir: QuickTourThis) { - if (!this.quickTourThisDirectives.length) { - this.quickTourThisDirectives.push(dir) - } else { - this.insertSorted(0, dir) - } + constructor( + private overlay: Overlay, + ){ + + // todo remove in production + setTimeout(() => { + this.startTour() + }, 2500) } - insertSorted(i, dir) { - if (dir.order > this.quickTourThisDirectives[i].order) { - if (!this.quickTourThisDirectives[i+1]) { - return this.quickTourThisDirectives.push(dir) - } else if (dir.order <= this.quickTourThisDirectives[i+1].order) { - return this.quickTourThisDirectives.splice(i+1, 0, dir) - } else { - this.insertSorted(i+1, dir) - } - } else { - this.quickTourThisDirectives.splice(i, 0, dir) - } + public register(dir: QuickTourThis) { + + this.slides.insertAfter( + dir, + tourObj => tourObj.order < dir.order + ) } - public unregister (dir: QuickTourThis) { - this.quickTourThisDirectives = this.quickTourThisDirectives.filter(d => d !== dir) + public unregister(dir: QuickTourThis){ + this.slides.remove(dir) } public startTour() { - this.currSlideNum = 0 - this.currentTip$.next(this.quickTourThisDirectives[this.currSlideNum]) + if (!this.overlayRef) { + this.overlayRef = this.overlay.create({ + height: '0px', + width: '0px', + hasBackdrop: true, + backdropClass: ['pe-none', 'cdk-overlay-dark-backdrop'], + positionStrategy: this.overlay.position().global(), + }) + } + + if (!this.cmpRef) { + this.cmpRef = this.overlayRef.attach( + new ComponentPortal(QuickTourComponent) + ) + + this.currActiveSlide = this.slides.first + this.currentTip$.next(this.currActiveSlide) + } } - public nextSlide() { - this.currSlideNum++ - this.currentTip$.next(this.quickTourThisDirectives[this.currSlideNum]) + public endTour() { + if (this.overlayRef) { + this.overlayRef.dispose() + this.overlayRef = null + this.cmpRef = null + } + } + public nextSlide() { + this.currActiveSlide = this.currActiveSlide.next + this.currentTip$.next(this.currActiveSlide) } public previousSlide() { - this.currSlideNum-- - this.currentTip$.next(this.quickTourThisDirectives[this.currSlideNum]) + this.currActiveSlide = this.currActiveSlide.prev + this.currentTip$.next(this.currActiveSlide) } - changeDetected(order) { - if (this.currentTip$.value && order === this.currentTip$.value.order) { - this.currentTip$.next(this.quickTourThisDirectives[this.currSlideNum]) + changeDetected(dir: QuickTourThis) { + if (this.currActiveSlide?.thisObj === dir) { + this.detectChanges$.next(null) } } } diff --git a/src/ui/quickTour/quickTourComponent/quickTour.component.ts b/src/ui/quickTour/quickTourComponent/quickTour.component.ts new file mode 100644 index 000000000..60a2f2d1f --- /dev/null +++ b/src/ui/quickTour/quickTourComponent/quickTour.component.ts @@ -0,0 +1,324 @@ +import { + Component, + HostBinding, + HostListener, + SecurityContext, + TemplateRef, +} from "@angular/core"; +import { Subscription } from "rxjs"; +import { QuickTourService } from "../quickTour.service"; +import { debounceTime, map, shareReplay} from "rxjs/operators"; +import { DomSanitizer } from "@angular/platform-browser"; +import { QuickTourThis } from "../quickTourThis.directive"; +import { clamp } from "src/util/generator"; + +@Component({ + templateUrl : './quickTour.template.html', + styleUrls : [ + './quickTour.style.css' + ] +}) +export class QuickTourComponent { + + static TourCardMargin = 24 + static TourCardWidthPx = 256 + static TourCardHeightPx = 64 + + public tourCardWidth = `${QuickTourComponent.TourCardWidthPx}px` + public arrowTmpl: TemplateRef<any> + public arrowSrc: string + + @HostListener('window:keydown', ['$event']) + keydownListener(ev: KeyboardEvent){ + if (ev.key === 'Escape') { + this.quickTourService.endTour() + } + } + + @HostBinding('style.align-items') + alignItems: 'center' | 'flex-start' | 'flex-end' = 'center' + + @HostBinding('style.justify-content') + justifyContent: 'center' | 'flex-start' | 'flex-end' = 'center' + + @HostBinding('style.transform') + transform = this.sanitizer.bypassSecurityTrustStyle(`translate(200px, -450px)`) + + public tourCardTransform = `translate(0px, 0px)` + public customArrowTransform = `translate(0px, 0px)` + + public arrowFrom: [number, number] = [0, 0] + public arrowTo: [number, number] = [0, 0] + public arrowType: 'straight' | 'concave-from-top' | 'concave-from-bottom' = 'straight' + + private subscriptions: Subscription[] = [] + private currTipLinkedObj$ = this.quickTourService.currentTip$.pipe( + shareReplay(1) + ) + + public isLast$ = this.currTipLinkedObj$.pipe( + map(val => !val.next) + ) + + public isFirst$ = this.currTipLinkedObj$.pipe( + map(val => !val.prev) + ) + + public description$ = this.currTipLinkedObj$.pipe( + map(val => val.thisObj.description) + ) + + public overwrittenPosition = this.currTipLinkedObj$.pipe( + map(val => val.thisObj.overwritePosition) + ) + + private currTip: QuickTourThis + + constructor( + public quickTourService: QuickTourService, + private sanitizer: DomSanitizer, + ) { + + this.subscriptions.push( + + this.quickTourService.detectChanges$.pipe( + /** + * the debounce does two things: + * - debounce expensive calculate transform call + * - allow change detection to finish rendering element + */ + debounceTime(16) + ).subscribe(() => { + this.calculateTransforms() + }), + + this.quickTourService.currentTip$.pipe( + /** + * subscriber is quite expensive. + * only calculate at most once every 16 ms + */ + debounceTime(16) + ).subscribe(linkedObj => { + this.arrowTmpl = null + this.arrowSrc = null + this.currTip = null + + if (!linkedObj) { + // exit quick tour? + return + } + this.currTip = linkedObj.thisObj + this.calculateTransforms() + }) + ) + } + + nextSlide(){ + this.quickTourService.nextSlide() + } + + prevSlide(){ + this.quickTourService.previousSlide() + } + + endTour(){ + this.quickTourService.endTour() + } + + calculateTransforms() { + if (!this.currTip) { + return + } + const tip = this.currTip + + if (tip.overWriteArrow) { + if (typeof tip.overWriteArrow === 'string') { + this.arrowSrc = tip.overWriteArrow + } else { + this.arrowTmpl = tip.overWriteArrow + } + } + + if (tip.overwritePosition) { + this.alignItems = 'flex-start' + this.justifyContent = 'flex-start' + const { dialog, arrow } = tip.overwritePosition + const { top: dialogTop, left: dialogLeft } = dialog + const { + top: arrowTop, + left: arrowLeft, + } = arrow + this.tourCardTransform = this.sanitizer.sanitize( + SecurityContext.STYLE, + `translate(${dialogLeft}, ${dialogTop})` + ) + this.customArrowTransform = this.sanitizer.sanitize( + SecurityContext.STYLE, + `translate(${arrowLeft}, ${arrowTop})` + ) + return + } + + const { x: hostX, y: hostY, width: hostWidth, height: hostHeight } = tip.getHostPos() + const { innerWidth, innerHeight } = window + + const { position: tipPosition } = this.currTip + let translate: {x: number, y: number} = { x: hostX + hostWidth / 2, y: hostY + hostHeight / 2 } + + const hostCogX = hostX + hostWidth / 2 + const hostCogY = hostY + hostHeight / 2 + + let calcedPos: string = '' + + /** + * if position is unspecfied, try to figure out position + */ + if (!tipPosition) { + + // if host centre of grav is to the right of the screen + // position tour el to the left, otherwise, right + calcedPos += hostCogX > (innerWidth / 2) + ? 'left' + : 'right' + + // if host centre of grav is to the bottom of the screen + // position tour el to the top, otherwise, bottom + calcedPos += hostCogY > (innerHeight / 2) + ? 'top' + : 'bottom' + } + + /** + * if the directive specified where helper should appear + * set the offset directly + */ + + const usePosition = tipPosition || calcedPos + + /** + * default behaviour: center + * overwrite if align keywords appear + */ + this.alignItems = 'center' + this.justifyContent = 'center' + + if (usePosition.includes('top')) { + translate.y = hostY + this.alignItems = 'flex-end' + } + if (usePosition.includes('bottom')) { + translate.y = hostY + hostHeight + this.alignItems = 'flex-start' + } + if (usePosition.includes('left')) { + translate.x = hostCogX + this.justifyContent = 'flex-end' + } + if (usePosition.includes('right')) { + translate.x = hostCogX + this.justifyContent = 'flex-start' + } + + this.transform = this.sanitizer.sanitize( + SecurityContext.STYLE, + `translate(0px, 0px)` + ) + + /** + * set tour card transform + * set a given margin, so + */ + const tourCardMargin = QuickTourComponent.TourCardMargin + const tourCardWidth = QuickTourComponent.TourCardWidthPx + const tourCardHeight = QuickTourComponent.TourCardHeightPx + /** + * catch if element is off screen + * clamp it inside the viewport + */ + const tourCardTranslate = [ + clamp(translate.x, 0, innerWidth), + clamp(translate.y, 0, innerHeight), + ] + if (usePosition.includes('top')) { + tourCardTranslate[1] += -1 * tourCardMargin + } + if (usePosition.includes('bottom')) { + tourCardTranslate[1] += tourCardMargin + } + if (usePosition.includes('left')) { + tourCardTranslate[0] += -1 * tourCardMargin + } + if (usePosition.includes('right')) { + tourCardTranslate[0] += tourCardMargin + } + this.tourCardTransform = `translate(${tourCardTranslate[0]}px, ${tourCardTranslate[1]}px)` + + /** + * set arrow from / to + */ + + const { + arrowTo + } = (() => { + if (usePosition.includes('top')) { + return { + arrowTo: [ hostCogX, hostY ] + } + } + if (usePosition.includes('bottom')) { + return { + arrowTo: [ hostCogX, hostY + hostHeight ] + } + } + if (usePosition.includes('left')) { + return { + arrowTo: [ hostX, hostCogY ] + } + } + if (usePosition.includes('right')) { + return { + arrowTo: [ hostX + hostWidth, hostCogY ] + } + } + })() + + + const arrowFrom = [ arrowTo[0], arrowTo[1] ] + + if (usePosition.includes('top')) { + arrowFrom[1] -= tourCardMargin + (tourCardHeight / 2) + this.arrowType = 'concave-from-bottom' + } + if (usePosition.includes('bottom')) { + arrowFrom[1] += tourCardMargin + (tourCardHeight / 2) + this.arrowType = 'concave-from-top' + } + if (usePosition.includes('left')) { + arrowFrom[0] -= tourCardMargin + this.arrowType = 'straight' + } + if (usePosition.includes('right')) { + arrowFrom[0] += tourCardMargin + this.arrowType = 'straight' + } + this.arrowFrom = arrowFrom as [number, number] + this.arrowTo = arrowTo as [number, number] + + /** + * set arrow type + */ + + this.arrowType = 'straight' + + if (usePosition.includes('top')) { + this.arrowType = 'concave-from-bottom' + } + if (usePosition.includes('bottom')) { + this.arrowType = 'concave-from-top' + } + } + + handleWindowResize(){ + this.calculateTransforms() + } +} diff --git a/src/ui/quickTour/quickTourComponent/quickTour.style.css b/src/ui/quickTour/quickTourComponent/quickTour.style.css new file mode 100644 index 000000000..c6d9f7bc0 --- /dev/null +++ b/src/ui/quickTour/quickTourComponent/quickTour.style.css @@ -0,0 +1,23 @@ +:host +{ + display: inline-flex; + width: 0px; + height: 0px; + position: relative; +} + +mat-card +{ + position: absolute; + margin: 0; + z-index: 10; +} + +.custom-svg >>> svg +{ + pointer-events: none; + stroke-width: 3px; + stroke: rgb(255, 255, 255); + stroke-linecap: round; + stroke-linejoin: round; +} diff --git a/src/ui/quickTour/quickTourComponent/quickTour.template.html b/src/ui/quickTour/quickTourComponent/quickTour.template.html new file mode 100644 index 000000000..af7277221 --- /dev/null +++ b/src/ui/quickTour/quickTourComponent/quickTour.template.html @@ -0,0 +1,43 @@ +<mat-card + iav-window-resize + [iav-window-resize-time]="64" + (iav-window-resize-event)="handleWindowResize()" + [style.width]="tourCardWidth" + [style.transform]="tourCardTransform"> + <mat-card-content> + {{ description$ | async }} + </mat-card-content> + <mat-card-actions> + <button mat-icon-button + (click)="prevSlide()" + [disabled]="isFirst$ | async"> + <i class="fas fa-chevron-left"></i> + </button> + <button mat-icon-button + (click)="nextSlide()" + [disabled]="isLast$ | async"> + <i class="fas fa-chevron-right"></i> + </button> + <button mat-icon-button + (click)="endTour()"> + <i class="fas fa-times"></i> + </button> + </mat-card-actions> +</mat-card> + +<div *ngIf="arrowTmpl" [style.transform]="customArrowTransform" + class="custom-svg"> + <ng-container *ngTemplateOutlet="arrowTmpl"> + </ng-container> +</div> + +<ng-template [ngIf]="arrowSrc"> + arrow src not yet implmented +</ng-template> + +<quick-tour-arrow + *ngIf="!arrowTmpl && !arrowSrc" + [quick-tour-arrow-to]="arrowTo" + [quick-tour-arrow-from]="arrowFrom" + [quick-tour-arrow-type]="arrowType"> +</quick-tour-arrow> \ No newline at end of file diff --git a/src/ui/quickTour/quickTourThis.directive.ts b/src/ui/quickTour/quickTourThis.directive.ts index bda24c7a9..32054b1a5 100644 --- a/src/ui/quickTour/quickTourThis.directive.ts +++ b/src/ui/quickTour/quickTourThis.directive.ts @@ -1,33 +1,38 @@ -import {Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges} from "@angular/core"; -import {QuickTourService} from "src/ui/quickTour/quickTour.service"; -import {QuickTourPosition} from "src/ui/quickTour/constrants"; +import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, TemplateRef } from "@angular/core"; +import { QuickTourService } from "src/ui/quickTour/quickTour.service"; +import { IQuickTourOverwritePosition, TQuickTourPosition } from "src/ui/quickTour/constrants"; @Directive({ - selector: '[quick-tour]' + selector: '[quick-tour]', + exportAs: 'quickTour' }) export class QuickTourThis implements OnInit, OnChanges, OnDestroy { - @Input('quick-tour-order') order: number = 0 - @Input('quick-tour-description') description: string = 'No description' - @Input('quick-tour-position') position: QuickTourPosition + @Input('quick-tour-order') order: number = 0 + @Input('quick-tour-description') description: string = 'No description' + @Input('quick-tour-position') position: TQuickTourPosition + @Input('quick-tour-overwrite-position') overwritePosition: IQuickTourOverwritePosition + @Input('quick-tour-overwrite-arrow') overWriteArrow: TemplateRef<any> | string + + constructor( + private quickTourService: QuickTourService, + private el: ElementRef + ) {} - constructor(private quickTourService: QuickTourService, - private el: ElementRef) {} + public getHostPos() { + const { x, y, width, height } = (this.el.nativeElement as HTMLElement).getBoundingClientRect() + return { x, y, width, height } + } - public getHostPos() { - const { x, y, width, height } = (this.el.nativeElement as HTMLElement).getBoundingClientRect() - return {x, y, width, height} - } + ngOnInit() { + this.quickTourService.register(this) + } - ngOnInit() { - this.quickTourService.register(this) - } + ngOnChanges() { + this.quickTourService.changeDetected(this) + } - ngOnChanges(changes: SimpleChanges) { - this.quickTourService.changeDetected(this.order) - } - - ngOnDestroy() { - this.quickTourService.unregister(this) - } + ngOnDestroy() { + this.quickTourService.unregister(this) + } } diff --git a/src/ui/quickTour/quickToutComponent/quickTour.component.ts b/src/ui/quickTour/quickToutComponent/quickTour.component.ts deleted file mode 100644 index 648fe1f12..000000000 --- a/src/ui/quickTour/quickToutComponent/quickTour.component.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { - AfterViewInit, - Component, - ElementRef, - EventEmitter, - OnInit, - Output, ViewChild -} from "@angular/core"; -import {Subscription} from "rxjs"; -import {QuickTourService} from "src/ui/quickTour/quickTour.service"; -import {map} from "rxjs/operators"; -import {QuickTourPosition} from "src/ui/quickTour/constrants"; - -@Component({ - selector : 'quick-tour', - templateUrl : './quickTour.temlate.html', - styleUrls : ['./quickTour.style.css',], -}) -export class QuickTourComponent implements OnInit, AfterViewInit { - - public slideWidth = 300 - private tip: any - - public isLast = false - public left: number - public top: number - private topHardcoded = false - private leftHardcoded = false - public description: string - public order: number - - private targetElWidth: number - private targetElHeight: number - - public position: QuickTourPosition - - public tipHidden = true - public flexClass: string - public positionChangedByArrow = false - - private subscriptions: Subscription[] = [] - - @Output() destroy = new EventEmitter() - - @ViewChild('tipCard') tipCardEl: ElementRef - @ViewChild('arrowEl') arrowEl: ElementRef - - constructor(public quickTourService: QuickTourService) {} - - ngOnInit() { - this.subscriptions.push( - this.quickTourService.currentTip$.pipe( - - map(tip => { - if (!tip) return - this.clear() - this.tip = tip - this.description = tip.description - this.position = tip.position - this.order = tip.order - this.isLast = tip === [...this.quickTourService.quickTourThisDirectives].pop() - this.setFlexClass() - - this.calculate() - if (tip.position.left) { - this.leftHardcoded = true - this.left = tip.position.left - } - if (tip.position.top) { - this.topHardcoded = true - this.top = tip.position.top - } - - setTimeout(() => this.calculateArrowPosition()) - - - }), - ).subscribe() - ) - } - - ngAfterViewInit() { - this.calculate() - } - - clear() { - this.tip = null - this.left = null - this.topHardcoded = false - this.leftHardcoded = false - this.top = null - this.description = null - this.position = null - this.order = null - this.targetElWidth = null - this.targetElHeight = null - this.positionChangedByArrow = false - this.tipHidden = true - this.flexClass = null - } - - calculate() { - const tip = this.tip - if (tip === null) return - - const { x, y, width, height } = tip.getHostPos() - - this.targetElWidth = width - this.targetElHeight = height - - if (this.tipCardEl) setTimeout(() => { - if (!this.leftHardcoded) { - this.left = x - this.calculatePosition('left', tip, width, height) - } - if (!this.topHardcoded) { - this.top = y - this.calculatePosition('top', tip, width, height) - } - }) - } - - calculatePosition(calculate: 'top' | 'left', tip, elementWidth, elementHeight) { - const cardWidth = this.tipCardEl.nativeElement.offsetWidth - const cardHeight = this.tipCardEl.nativeElement.offsetHeight - const calculateTop = calculate === 'top' - const calculateLeft = calculate === 'left' - - if (tip.position?.position) { - if (tip.position.position.includes('top') && calculateTop) { - this.top -= cardHeight - } - if (tip.position.position.includes('right') && calculateLeft) { - this.left += elementWidth - } - if (tip.position.position.includes('bottom') && calculateTop) { - this.top += elementHeight - } - if (tip.position.position.includes('left') && calculateLeft) { - this.left -= cardWidth - } - } - if (tip.position.align) { - if (tip.position.align === 'right' && calculateLeft) { - this.left -= cardWidth - elementWidth - } - if (tip.position.align === 'bottom' && calculateTop) { - this.top -= cardHeight - elementHeight - } - if (tip.position.align === 'center') { - if (['right', 'left'].includes(tip.position.position) && calculateTop) { - this.top -= (cardHeight - elementHeight)/2 - } - if (['bottom', 'top'].includes(tip.position.position) && calculateLeft) { - this.left -= (cardWidth - elementWidth)/2 - } - } - } - } - - calculateArrowPosition() { - if (['top-left', 'top-right'].includes(this.position.arrowPosition)) { - if (!this.position.arrowMargin || !this.position.arrowMargin.top) { - this.position.arrowMargin = {} - this.position.arrowMargin.top = 0 - } - this.position.arrowMargin.top += -this.tipCardEl.nativeElement.offsetHeight - - if (!this.positionChangedByArrow) this.top += this.tipCardEl.nativeElement.offsetHeight - this.positionChangedByArrow = true - } - - if (['bottom-left', 'bottom-right'].includes(this.position.arrowPosition)) { - if (!this.position.arrowMargin || !this.position.arrowMargin.top) { - this.position.arrowMargin = {} - this.position.arrowMargin.top = 0 - } - this.position.arrowMargin.top += this.tipCardEl.nativeElement.offsetHeight - } - - if (['bottom-left', 'bottom-right', 'bottom'].includes(this.position.arrowPosition)) { - if (!this.positionChangedByArrow) this.top -= this.arrowEl.nativeElement.offsetHeight - this.positionChangedByArrow = true - } - - this.tipHidden = false - } - - setFlexClass() { - const position = this.tip.position - this.flexClass = position?.arrowPosition.includes('right')? - position?.arrowAlign === 'top'? 'flex-row-reverse align-items-start' : - position?.arrowAlign === 'center'? 'flex-row-reverse align-items-center' : - position?.arrowAlign === 'bottom'? 'flex-row-reverse align-items-end' : - position?.arrowPosition.includes('top')? 'align-items-start' : 'flex-row-reverse align-items-end' - :position?.arrowPosition.includes('left')? - position?.arrowAlign === 'top'? 'flex-row align-items-start' : - position?.arrowAlign === 'center'? 'flex-row align-items-center' : - position?.arrowAlign === 'bottom'? 'flex-row align-items-end' : - position?.arrowPosition.includes('top')? 'align-items-start' : 'align-items-end' - :position?.arrowPosition.includes('top')? - position?.arrowAlign === 'left'? 'flex-column align-items-start' : - position?.arrowAlign === 'center'? 'flex-column align-items-center' : - position?.arrowAlign === 'right'? 'flex-column align-items-end' : - position?.arrowPosition.includes('left')? 'align-items-start' : 'align-items-end' - :position?.arrowPosition.includes('bottom')? - position?.arrowAlign === 'left'? 'flex-column-reverse align-items-end' : - position?.arrowAlign === 'center'? 'flex-column-reverse align-items-center' : - position?.arrowAlign === 'right'? 'flex-column-reverse align-items-end' : - position?.arrowPosition.includes('left')? 'align-items-start' : 'align-items-end' : '' - } -} diff --git a/src/ui/quickTour/quickToutComponent/quickTour.style.css b/src/ui/quickTour/quickToutComponent/quickTour.style.css deleted file mode 100644 index 041c3bde2..000000000 --- a/src/ui/quickTour/quickToutComponent/quickTour.style.css +++ /dev/null @@ -1,77 +0,0 @@ -:host { - display: block; - width: 100%; - height: 100%; -} - -::ng-deep .cdk-global-overlay-wrapper { - z-index: 900; -} - -::ng-deep .mat-stroked-button[disabled] { - color: rgba(128, 128, 128, 0.41) !important; -} - -::ng-deep .mat-stroked-button:not([disabled]) { - border-color: #222529 !important; -} - -.stokeColor { - stroke-width: 3px; - stroke-linecap: round; - stroke-linejoin: round; - fill: rgba(0, 0, 0, 0); -} -:host-context([darktheme="true"]) .stokeColor { - stroke: rgb(255, 255, 255); -} -:host-context([darktheme="false"]) .stokeColor { - stroke: rgb(66,66,66); -} - -.tour-help { - z-index: 901; -} - -.tipCard { - border-radius: 10px; - text-align: left; -} - -:host-context([darktheme="true"]) .tipCard { - background-color: #424242; - color: white; -} -:host-context([darktheme="false"]) .tipCard { - background-color: #FFFFFF; - border: 1px solid rgba(66, 66, 66, 0.26); - color: #424242; -} - -.cover { - background-color: rgba(0,0,0, 0.5); - width: 100vw; - height: 100vh; - z-index: 899; - - top: 0; - left: 0; -} - -.box { - box-sizing: content-box; - width: 0; - height: 0; - transform-origin: top left; - border: 10000px solid rgba(0, 0, 0, 0.5); -} - -.inner-box { - border: 1px white solid; -} - -.stepCircle { - width: 10px; - height: 10px; - border-radius: 5px; -} diff --git a/src/ui/quickTour/quickToutComponent/quickTour.temlate.html b/src/ui/quickTour/quickToutComponent/quickTour.temlate.html deleted file mode 100644 index d78a10c98..000000000 --- a/src/ui/quickTour/quickToutComponent/quickTour.temlate.html +++ /dev/null @@ -1,118 +0,0 @@ -<div class="tour-help position-fixed d-flex" - [ngStyle]="{ - visibility: tipHidden? 'hidden' : 'visible', - top: top + 'px', - left: left + 'px'}" - [ngClass]="[flexClass || '']"> - - - <div #arrowEl [ngStyle]="{ - marginLeft: this.position?.arrowMargin?.left && this.position.arrowMargin.left + 'px', - marginRight: this.position?.arrowMargin?.right && this.position.arrowMargin.right + 'px', - marginTop: this.position?.arrowMargin?.top && this.position.arrowMargin.top + 'px', - marginBottom: this.position?.arrowMargin?.bottom && this.position.arrowMargin.bottom + 'px'}"> - <ng-content *ngTemplateOutlet="arrow"></ng-content> - </div> - - <div #tipCard class="d-flex align-items-center justify-content-center h-100 tipCard pt-3 pr-3 pl-3" - [ngStyle]="{width: slideWidth + 'px'}"> - - <div class="iv-custom-comp d-flex flex-column"> - <h6 class="mb-2"> - {{description}} - </h6> - - <div class="text-center"> - <button mat-button iav-stop="mousedown mouseup" - (click)="quickTourService.previousSlide()" - *ngIf="order > 0"> - <i class="fas fa-angle-left"></i> - <span class="ml-1">BACK</span> - </button> - - <button mat-button iav-stop="mousedown mouseup" - (click)="quickTourService.nextSlide()" - *ngIf="!isLast" - > - <span class="mr-1">NEXT</span> - <i class="fas fa-angle-right"></i> - </button> - - <button iav-stop="mousedown mouseup" - (click)="destroy.emit();" - mat-button>CLOSE</button> - - </div> - - <div class="d-flex align-self-end mb-1 mt-1"> - <div *ngFor="let stepCircle of quickTourService.quickTourThisDirectives; let i = index" - class="border mr-1 stepCircle" - [ngStyle]="{background: quickTourService.currSlideNum === i? 'currentColor' : ''}"> - </div> - </div> - - </div> - </div> -</div> - - -<ng-template #arrow> - <ng-content *ngTemplateOutlet="position?.arrow === 'arrow2'? arrow2: - position?.arrow === 'arrow3'? arrow3: - position?.arrow === 'arrow4'? arrow4: - position?.arrow === 'arrow5'? arrow5: - arrow1"></ng-content> -</ng-template> - -<ng-template #arrow1> - <svg [ngStyle]="{transform: position?.arrowTransform? position?.arrowTransform : null}" - height="80px" class="overflow-visible" viewBox="166.586 231.427 12.015 71.497" xmlns="http://www.w3.org/2000/svg"> - <path class="stokeColor" style="fill-opacity: 0; paint-order: stroke markers; stroke-linecap: round; stroke-linejoin: round; stroke-width: 3px;" d="M 183.239 326.14 C 176.831 309.209 176.634 285.376 182.647 254.643" transform="matrix(1, 0, 0, 1, -9.368863, -23.215925)"/> - <path class="stokeColor" d="M 187.97 263.39 C 187.12 262.245 184.536 259.536 182.665 254.732" transform="matrix(1, 0, 0, 1, -9.368863, -23.215925)"/> - <path class="stokeColor" d="M 182.791 254.976 C 181.626 254.227 174.925 262.146 176.09 262.895" transform="matrix(1, 0, 0, 1, -9.368863, -23.215925)"/> - </svg> -</ng-template> - -<ng-template #arrow2> - <svg [ngStyle]="{transform: position?.arrowTransform? position?.arrowTransform : null}" - width="80px" height="50px" class="overflow-visible" viewBox="150.239 144.229 67.46 40.886" xmlns="http://www.w3.org/2000/svg"> - <path class="stokeColor" d="M 296.081 265.092 Q 302.545 229.649 359.476 224.337" transform="matrix(1, 0, 0, 1, -141.776566, -80.108154)"/> - <path class="stokeColor" d="M 292.065 265.677 C 294.922 265.86 297.891 264.941 300.918 262.844 C 302.986 261.411 299.162 257.842 295.976 253.078" transform="matrix(0, 1, -1, 0, 415.938782, -116.389183)"/> - </svg> -</ng-template> - -<ng-template #arrow3> - <svg [ngStyle]="{transform: position?.arrowTransform? position?.arrowTransform : null}" - height="100px" - viewBox="23.011 51.12 86.091 84.486" xmlns="http://www.w3.org/2000/svg"> - <path class="stokeColor" d="M 42.372 135.275 C 39.099 72.198 70.666 69.778 107.477 70.17"/> - <path class="stokeColor" d="M 28.277 125.699 L 41.622 125.699" transform="matrix(0.939693, -0.34202, 0.34202, 0.939693, -40.883873, 19.534008)"/> - <path class="stokeColor" d="M 33.304 121.099 L 25.073 128.035 L 33.062 134.606" transform="matrix(0.939693, -0.34202, 0.34202, 0.939693, -41.967846, 17.693502)"/> - <path class="stokeColor" d="M 92.404 63.192 L 105.749 63.192" transform="matrix(-0.34202, 0.939693, -0.939693, -0.34202, 192.343719, -8.296521)"/> - <path class="stokeColor" d="M 105.464 50.544 L 97.233 57.48 L 105.222 64.051" transform="matrix(-0.34202, 0.939693, -0.939693, -0.34202, 189.85376, -18.342039)"/> - <path class="stokeColor" d="M 42.968 77.934 L 56.313 77.934" transform="matrix(0.743145, 0.669131, -0.669131, 0.743145, 64.89846, -13.198224)"/> - <path class="stokeColor" d="M 49.142 66.866 L 40.911 73.802 L 48.9 80.373" transform="matrix(0.743145, 0.669131, -0.669131, 0.743145, 60.826363, -11.219056)"/> - </svg> - -</ng-template> - -<ng-template #arrow4> - <svg [ngStyle]="{transform: position?.arrowTransform? position?.arrowTransform : null}" - height="50px" class="overflow-visible" viewBox="88.429 331.463 16.292 53.247" xmlns="http://www.w3.org/2000/svg"> - <path class="stokeColor" style="fill-rule: evenodd; fill: rgba(0, 0, 0, 0); stroke-width: 3px;" d="M 96.649 384.71 L 96.649 332.904"/> - <path class="stokeColor" style="fill-rule: evenodd; fill: rgba(0, 0, 0, 0); paint-order: fill; stroke-linejoin: round; stroke-linecap: round; stroke-width: 3px;" d="M 88.708 343.497 C 86.828 341.857 94.988 329.978 96.868 331.618 C 99.945 331.127 106.222 342.826 104.392 343.118"/> - </svg> -</ng-template> - -<ng-template #arrow5> - <svg [ngStyle]="{transform: position?.arrowTransform? position?.arrowTransform : null}" - width="50px" viewBox="140.601 179.981 53.247 16.292" xmlns="http://www.w3.org/2000/svg"> - <path class="stokeColor" style="fill-rule: evenodd; fill: rgba(0, 0, 0, 0); stroke-width: 3px;" d="M 167.945 213.956 L 167.945 162.15" transform="matrix(0, -1, 1, 0, -20.107986, 355.997986)"/> - <path class="stokeColor" style="fill-rule: evenodd; fill: rgba(0, 0, 0, 0); paint-order: fill; stroke-linejoin: round; stroke-linecap: round; stroke-width: 3px;" d="M 138.751 194.144 C 136.871 192.504 145.031 180.625 146.911 182.265 C 149.988 181.774 156.265 193.473 154.435 193.765" transform="matrix(0, -1, 1, 0, -41.509438, 334.744568)"/> - </svg> -</ng-template> - - - - - diff --git a/src/ui/topMenu/topMenuCmp/topMenu.components.ts b/src/ui/topMenu/topMenuCmp/topMenu.components.ts index 0dfa246f1..a15328676 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.components.ts +++ b/src/ui/topMenu/topMenuCmp/topMenu.components.ts @@ -1,18 +1,18 @@ import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, Input, TemplateRef, } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import {Observable, of} from "rxjs"; +import { Observable } from "rxjs"; import { map } from "rxjs/operators"; import { AuthService } from "src/auth"; import { IavRootStoreInterface, IDataEntry } from "src/services/stateStore.service"; import { MatDialog, MatDialogConfig, MatDialogRef } from "@angular/material/dialog"; import { MatBottomSheet } from "@angular/material/bottom-sheet"; import { CONST, QUICKTOUR_DESC } from 'common/constants' +import { IQuickTourData } from "src/ui/quickTour/constrants"; @Component({ selector: 'top-menu-cmp', @@ -51,10 +51,9 @@ export class TopMenuCmp { public pluginTooltipText: string = `Plugins and Tools` public screenshotTooltipText: string = 'Take screenshot' - quickTourData = { + public quickTourData: IQuickTourData = { description: QUICKTOUR_DESC.TOP_MENU, order: 8, - position: {position: 'bottom', align: 'center', arrowPosition: 'top', arrowAlign: 'center', arrowTransform: 'scaleX(-1)'} } constructor( @@ -62,7 +61,6 @@ export class TopMenuCmp { private authService: AuthService, private dialog: MatDialog, public bottomSheet: MatBottomSheet, - private changeDetectionRef: ChangeDetectorRef, ) { this.user$ = this.authService.user$ diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html index 8b4f55c35..aee22de81 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -42,11 +42,10 @@ <ng-template #fullTmpl> <div class="d-flex flex-row-reverse" - quick-tour - [quick-tour-description]="quickTourData.description" - [quick-tour-order]="quickTourData.order" - [quick-tour-position]="quickTourData.position" - [iav-key-listener]="keyListenerConfig" + quick-tour + [quick-tour-description]="quickTourData.description" + [quick-tour-order]="quickTourData.order" + [iav-key-listener]="keyListenerConfig" (iav-key-event)="openTmplWithDialog(helperOnePager)"> <!-- signin --> diff --git a/src/util/LinkedList.ts b/src/util/LinkedList.ts new file mode 100644 index 000000000..2d57658f7 --- /dev/null +++ b/src/util/LinkedList.ts @@ -0,0 +1,94 @@ +export interface IDoublyLinkedItem<T> { + next: IDoublyLinkedItem<T> + prev: IDoublyLinkedItem<T> + thisObj: T +} + +export class DoublyLinkedList<T extends object>{ + + public first: IDoublyLinkedItem<T> + public last: IDoublyLinkedItem<T> + private _map = new WeakMap<T, IDoublyLinkedItem<T>>() + private _size: number = 0 + insertAfter(element: T, predicate: (cmpObj: T) => boolean){ + if (this._map.get(element)) { + console.warn(`element has already been added to the doublylinkedlist`) + return + } + let newDoublyLinkedItem: IDoublyLinkedItem<T> + + const insertAfter = FindInLinkedList<T>( + this, + predicate + ) + + /** + * if predicate can be found, then insert after the found entry + * if not, then the previous first entry will be the next element + */ + const newDoublyLinkedItemNext = insertAfter + ? insertAfter.next + : this.first + + newDoublyLinkedItem = { + prev: insertAfter, + next: newDoublyLinkedItemNext, + thisObj: element + } + + /** + * set next of prev item + * if prev is null, set first as this doublyitem + */ + if (insertAfter) insertAfter.next = newDoublyLinkedItem + else this.first = newDoublyLinkedItem + + /** + * set prev of next item + * if next is null, set last as this doublyitem + */ + if (newDoublyLinkedItemNext) newDoublyLinkedItemNext.prev = newDoublyLinkedItem + else this.last = newDoublyLinkedItem + + this._map.set(element, newDoublyLinkedItem) + this._size ++ + } + remove(element: T) { + const doublyLinkedItem = this._map.get(element) + if (!doublyLinkedItem) { + console.error(`doubly linked item not found`) + return + } + const { next, prev } = doublyLinkedItem + + if (prev) prev.next = next + if (next) next.prev = prev + + if (doublyLinkedItem === this.first) this.first = this.first.next + if (doublyLinkedItem === this.last) this.last = this.last.prev + + // weakmap does not need to explicitly remove key/val + // decrement the size + this._size -- + } + size(){ + return this._size + } +} + +export function FindInLinkedList<T extends object>(list: DoublyLinkedList<T>, predicate: (element: T) => boolean){ + let compareObj = list.first, + returnObj: IDoublyLinkedItem<T> = null + + if (!compareObj) return null + + do { + if (predicate(compareObj.thisObj)) { + returnObj = compareObj + break + } + compareObj = compareObj.next + } while(!!compareObj) + + return returnObj +} diff --git a/src/util/index.ts b/src/util/index.ts index 25e810ea7..eeb79967a 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,3 +1,8 @@ export { UtilModule } from './util.module' export { PureContantService } from './pureConstant.service' export { CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor } from './injectionTokens' +export { + DoublyLinkedList, + FindInLinkedList, + IDoublyLinkedItem +} from './LinkedList' diff --git a/src/util/windowResize/index.ts b/src/util/windowResize/index.ts new file mode 100644 index 000000000..cd22fb2f4 --- /dev/null +++ b/src/util/windowResize/index.ts @@ -0,0 +1,2 @@ +export { WindowResizeModule } from './module' +export { ResizeObserverDirective } from './windowResize.directive' diff --git a/src/util/windowResize/module.ts b/src/util/windowResize/module.ts new file mode 100644 index 000000000..61392d6f4 --- /dev/null +++ b/src/util/windowResize/module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { ResizeObserverDirective } from "./windowResize.directive"; +import { ResizeObserverService } from "./windowResize.service"; + +@NgModule({ + declarations: [ + ResizeObserverDirective + ], + exports: [ + ResizeObserverDirective + ], + providers: [ + ResizeObserverService + ] +}) + +export class WindowResizeModule{} diff --git a/src/util/windowResize/windowResize.directive.ts b/src/util/windowResize/windowResize.directive.ts new file mode 100644 index 000000000..ce8de3180 --- /dev/null +++ b/src/util/windowResize/windowResize.directive.ts @@ -0,0 +1,64 @@ +import { Directive, EventEmitter, Input, OnChanges, OnInit, Output } from "@angular/core"; +import { Subscription } from "rxjs"; +import { ResizeObserverService } from "./windowResize.service"; + +@Directive({ + selector: '[iav-window-resize]', + exportAs: 'iavWindowResize' +}) + +export class ResizeObserverDirective implements OnChanges, OnInit { + @Input('iav-window-resize-type') + type: 'debounce' | 'throttle' = 'throttle' + + @Input('iav-window-resize-time') + time: number = 160 + + @Input('iav-window-resize-throttle-leading') + throttleLeading = false + + @Input('iav-window-resize-throttle-trailing') + throttleTrailing = true + + @Output('iav-window-resize-event') + ev: EventEmitter<Event> = new EventEmitter() + + private sub: Subscription[] = [] + + constructor(private svc: ResizeObserverService){} + + ngOnInit(){ + this.configure() + } + ngOnChanges(){ + this.configure() + } + + configure(){ + while(this.sub.length > 0) this.sub.pop().unsubscribe() + + let sub: Subscription + if (this.type === 'throttle') { + sub = this.svc.getThrottledResize( + this.time, + { + leading: this.throttleLeading, + trailing: this.throttleTrailing + } + ).subscribe(event => this.ev.emit(event)) + } + + if (this.type === 'debounce') { + sub = this.svc.getDebouncedResize( + this.time, + ).subscribe(event => this.ev.emit(event)) + } + + if (!this.type) { + sub = this.svc.windowResize.pipe( + ).subscribe(event => this.ev.emit(event)) + } + + this.sub.push(sub) + } +} diff --git a/src/util/windowResize/windowResize.service.ts b/src/util/windowResize/windowResize.service.ts new file mode 100644 index 000000000..f50c7df0e --- /dev/null +++ b/src/util/windowResize/windowResize.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from "@angular/core"; +import { asyncScheduler, fromEvent } from "rxjs"; +import { debounceTime, shareReplay, tap, throttleTime } from "rxjs/operators"; + +interface IThrottleConfig { + leading: boolean + trailing: boolean +} + +@Injectable({ + providedIn: 'root' +}) + +export class ResizeObserverService { + public windowResize = fromEvent(window, 'resize').pipe( + shareReplay(1) + ) + + public getThrottledResize(time: number, config?: IThrottleConfig){ + return this.windowResize.pipe( + throttleTime(time, asyncScheduler, config || { leading: false, trailing: true }), + ) + } + + public getDebouncedResize(time: number) { + return this.windowResize.pipe( + debounceTime(time) + ) + } +} diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts index 52617785c..fe162efc3 100644 --- a/src/viewerModule/nehuba/module.ts +++ b/src/viewerModule/nehuba/module.ts @@ -24,6 +24,7 @@ import { BehaviorSubject } from "rxjs"; import { StateModule } from "src/state"; import { AuthModule } from "src/auth"; import {QuickTourModule} from "src/ui/quickTour/module"; +import { WindowResizeModule } from "src/util/windowResize"; @NgModule({ imports: [ @@ -37,6 +38,7 @@ import {QuickTourModule} from "src/ui/quickTour/module"; ComponentsModule, MouseoverModule, ShareModule, + WindowResizeModule, /** * should probably break this into its own... diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 2e09e2117..39a33d414 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, ViewChild } from "@angular/core"; +import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, TemplateRef, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { asyncScheduler, combineLatest, fromEvent, interval, merge, Observable, of, Subject, timer } from "rxjs"; import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerActionSetPerspOctantRemoval, ngViewerActionToggleMax } from "src/services/state/ngViewerState/actions"; @@ -21,7 +21,7 @@ import { cvtNavigationObjToNehubaConfig, getFourPanel, getHorizontalOneThree, ge import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service"; import { MouseHoverDirective } from "src/mouseoverModule"; import { NehubaMeshService } from "../mesh.service"; -import {QuickTourData} from "src/ui/quickTour/constrants"; +import { IQuickTourData } from "src/ui/quickTour/constrants"; interface INgLayerInterface { name: string // displayName @@ -92,22 +92,19 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{ private findPanelIndex = (panel: HTMLElement) => this.viewPanelWeakMap.get(panel) public nanometersToOffsetPixelsFn: Array<(...arg) => any> = [] - public quickTourSliceViewSlide: QuickTourData = { + public quickTourSliceViewSlide: IQuickTourData = { order: 1, description: QUICKTOUR_DESC.SLICE_VIEW, - position: {position: 'bottom-right', arrow: 'arrow3', arrowPosition: 'left', arrowMargin: {right: -50, bottom: 130}}, } - public quickTour3dViewSlide: QuickTourData = { + public quickTour3dViewSlide: IQuickTourData = { order: 2, description: QUICKTOUR_DESC.PERSPECTIVE_VIEW, - position: {position: 'top-left', arrow: 'arrow5', arrowPosition: 'right', arrowAlign: 'bottom', arrowMargin: {bottom: -25, left: -10}, arrowTransform: 'rotate(-130deg)'}, } - public quickTourIconsSlide: QuickTourData = { + public quickTourIconsSlide: IQuickTourData = { order: 3, description: QUICKTOUR_DESC.VIEW_ICONS, - position: {position: 'bottom', align: 'right', arrow: 'arrow4', arrowPosition: 'top', arrowAlign: 'right', arrowMargin: {right: 50}}, } public customLandmarks$: Observable<any> = this.store$.pipe( @@ -120,6 +117,10 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{ }))), ) + ngAfterViewInit(){ + this.setQuickTourPos() + } + private forceUI$ = this.customLandmarks$.pipe( map(lm => { if (lm.length > 0) { @@ -833,4 +834,28 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{ ) } + public quickTourOverwritingPos = { + 'dialog': { + left: '0px', + top: '0px', + }, + 'arrow': { + left: '0px', + top: '0px', + } + } + + setQuickTourPos(){ + const { innerWidth, innerHeight } = window + this.quickTourOverwritingPos = { + 'dialog': { + left: `${innerWidth / 2}px`, + top: `${innerHeight / 2}px`, + }, + 'arrow': { + left: `${innerWidth / 2 - 48}px`, + top: `${innerHeight / 2 - 48}px`, + } + } + } } diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html index 13990f167..9108b1379 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html @@ -17,27 +17,26 @@ <current-layout *ngIf="viewerLoaded" class="position-absolute w-100 h-100 d-block pe-none top-0 left-0"> <div class="w-100 h-100 position-relative" cell-i - quick-tour - [quick-tour-description]="quickTourSliceViewSlide.description" - [quick-tour-order]="quickTourSliceViewSlide.order" - [quick-tour-position]="quickTourSliceViewSlide.position"> + iav-window-resize + [iav-window-resize-time]="64" + (iav-window-resize-event)="setQuickTourPos()" + quick-tour + [quick-tour-description]="quickTourSliceViewSlide.description" + [quick-tour-order]="quickTourSliceViewSlide.order" + [quick-tour-overwrite-arrow]="sliceViewArrow" + [quick-tour-overwrite-position]="quickTourOverwritingPos"> <ng-content *ngTemplateOutlet="ngPanelOverlayTmpl; context: { panelIndex: panelOrder$ | async | getNthElement : 0 | parseAsNumber }"></ng-content> </div> - <div class="w-100 h-100 position-relative" cell-ii - quick-tour - [quick-tour-description]="quickTourIconsSlide.description" - [quick-tour-order]="quickTourIconsSlide.order" - [quick-tour-position]="quickTourIconsSlide.position"> + <div class="w-100 h-100 position-relative" cell-ii> <ng-content *ngTemplateOutlet="ngPanelOverlayTmpl; context: { panelIndex: panelOrder$ | async | getNthElement : 1 | parseAsNumber }"></ng-content> </div> <div class="w-100 h-100 position-relative" cell-iii> <ng-content *ngTemplateOutlet="ngPanelOverlayTmpl; context: { panelIndex: panelOrder$ | async | getNthElement : 2 | parseAsNumber }"></ng-content> </div> <div class="w-100 h-100 position-relative" cell-iv - quick-tour - [quick-tour-description]="quickTour3dViewSlide.description" - [quick-tour-order]="quickTour3dViewSlide.order" - [quick-tour-position]="quickTour3dViewSlide.position"> + quick-tour + [quick-tour-description]="quickTour3dViewSlide.description" + [quick-tour-order]="quickTour3dViewSlide.order"> <ng-content *ngTemplateOutlet="ngPanelOverlayTmpl; context: { panelIndex: panelOrder$ | async | getNthElement : 3 | parseAsNumber }"></ng-content> </div> </current-layout> @@ -125,6 +124,13 @@ [attr.data-viewer-controller-visible]="visible" [attr.data-viewer-controller-index]="panelIndex"> + <div class="d-inline-block" + *ngIf="panelIndex === 1" + quick-tour + [quick-tour-description]="quickTourIconsSlide.description" + [quick-tour-order]="quickTourIconsSlide.order"> + </div> + <!-- perspective specific control --> <ng-container *ngIf="panelIndex === 3"> <ng-container *ngTemplateOutlet="perspectiveOctantRemovalTmpl; context: { @@ -182,3 +188,21 @@ </button> </div> </ng-template> + +<ng-template #sliceViewArrow> + <svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path id="quarter_circle" d="M22.6151 96.5C22.6151 96.5 18.5 84.1266 18.5 76.5C18.5001 62 18.1151 59.5 22.6151 47C27.115 34.5 39.3315 27.7229 47.5 25C56.5 22 63 22.5 72.5 24C83.1615 25.6834 83.5 26 91 29" /> + <g id="arrow_top_left"> + <path id="arrow_stem" d="M37 40C35.5882 38.5882 17.6863 20.6863 12 15" /> + <path id="arrow_head" d="M6 24C6.38926 21.7912 6.68496 18.3286 6.71205 16.0803C6.73751 13.9665 6.632 13.6135 6.52632 11.5C6.46368 10.2469 6.52632 11.5 6 8C11 10 9.71916 9.74382 11 9.99999C13.5 10.5 13.743 10.7451 17 11C20 11.2348 21.1276 11 22 11" stroke-linecap="round" stroke-linejoin="round"/> + </g> + <g id="arrow_left"> + <path id="arrow_stem_2" d="M29.4229 78.5771C27.1573 78.5771 18.3177 78.5771 9.19238 78.5771" /> + <path id="arrow_head_2" d="M13.3137 89.6274C12.0271 87.7903 9.78778 85.1328 8.2171 83.5238C6.74048 82.0112 6.41626 81.8362 4.84703 80.4164C3.91668 79.5747 4.84703 80.4164 2 78.3137C6.94975 76.1924 5.86291 76.9169 6.94974 76.1924C9.07106 74.7782 9.41624 74.7797 11.8995 72.6569C14.1868 70.7016 14.8181 69.7382 15.435 69.1213" stroke-linecap="round" stroke-linejoin="round"/> + </g> + <g id="arrow_top"> + <path id="arrow_stem_3" d="M77.0057 32.0057C77.0057 30.3124 77.0057 16.2488 77.0057 9.42862" /> + <path id="arrow_head_3" d="M68.4342 11.1429C69.9189 10.1032 72.0665 8.29351 73.3667 7.02421C74.5891 5.83091 74.7305 5.5689 75.8779 4.30076C76.5581 3.54892 75.8779 4.30076 77.5771 2C79.2914 6.00002 78.7059 5.12172 79.2915 6.00002C80.4343 7.71431 80.4331 7.99326 82.1486 10C83.7287 11.8485 84.5072 12.3587 85.0058 12.8572" stroke-linecap="round" stroke-linejoin="round"/> + </g> + </svg> +</ng-template> diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts index 4691b93cd..c0cc7d57e 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts @@ -5,12 +5,12 @@ import { TemplateRef, HostBinding, Optional, - Inject, ViewChild, ElementRef + Inject, } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { LoggingService } from "src/logging"; import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; -import {Observable, Subscription, of, combineLatest, BehaviorSubject} from "rxjs"; +import { Observable, Subscription, of, combineLatest } from "rxjs"; import { map, filter, startWith } from "rxjs/operators"; import { MatBottomSheet } from "@angular/material/bottom-sheet"; import { MatDialog } from "@angular/material/dialog"; @@ -20,7 +20,7 @@ import { viewerStateNavigationStateSelector, viewerStateSelectedTemplatePureSele import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; import { getNavigationStateFromConfig, NEHUBA_INSTANCE_INJTKN } from '../util' -import {QuickTourData, QuickTourPosition} from "src/ui/quickTour/constrants"; +import { IQuickTourData } from "src/ui/quickTour/constrants"; @Component({ selector : 'iav-cmp-viewer-nehuba-status', @@ -52,12 +52,9 @@ export class StatusCardComponent implements OnInit, OnChanges{ public useTouchInterface$: Observable<boolean> - public quickTourExpandedPosition: QuickTourPosition = {position: 'bottom', align: 'left', top: 150, arrow: 'arrow4', arrowPosition: 'top', arrowAlign: 'center'} - public quickTourCollapsedPosition: QuickTourPosition = {position: 'bottom', align: 'left', top: 60, arrowPosition: 'top', arrowAlign: 'center'} - public quickTourData: QuickTourData = { + public quickTourData: IQuickTourData = { description: QUICKTOUR_DESC.STATUS_CARD, order: 6, - position: this.quickTourCollapsedPosition } public SHARE_BTN_ARIA_LABEL = ARIA_LABELS.SHARE_BTN diff --git a/src/viewerModule/nehuba/statusCard/statusCard.template.html b/src/viewerModule/nehuba/statusCard/statusCard.template.html index d3639b7da..5e4d793a7 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.template.html +++ b/src/viewerModule/nehuba/statusCard/statusCard.template.html @@ -1,107 +1,107 @@ <div quick-tour - [quick-tour-description]="quickTourData.description" - [quick-tour-order]="quickTourData.order" - [quick-tour-position]="quickTourData.position"> -<mat-card class="expandedContainer p-2 pt-1" *ngIf="showFull; else showMin" - -> - <mat-card-content> - - <!-- reset --> - <div class="d-flex"> - <span class="flex-grow-0 d-flex align-items-center"> - Reset - </span> + [quick-tour-description]="quickTourData.description" + [quick-tour-order]="quickTourData.order" + #statusCardQT="quickTour"> + <mat-card *ngIf="showFull; else showMin" + class="expandedContainer p-2 pt-1"> + + <mat-card-content> + + <!-- reset --> + <div class="d-flex"> + <span class="flex-grow-0 d-flex align-items-center"> + Reset + </span> - <div class="flex-grow-1"></div> - - <button - mat-icon-button - (click)="resetNavigation({position:true})" - matTooltip="Reset position"> - <i class="iavic iavic-translation"></i> - </button> - - <button - mat-icon-button - (click)="resetNavigation({rotation:true})" - matTooltip="Reset rotation"> - <i class="iavic iavic-rotation"></i> - </button> - - <button - mat-icon-button - (click)="resetNavigation({zoom:true})" - matTooltip="Reset zoom"> - <i class="iavic iavic-scaling"></i> - </button> - - <mat-divider [vertical]="true"></mat-divider> - - <button mat-icon-button - [attr.aria-label]="HIDE_FULL_STATUS_PANEL_ARIA_LABEL" - (click)="showFull = false; quickTourData.position = quickTourCollapsedPosition"> - <i class="fas fa-angle-up"></i> - </button> - </div> + <div class="flex-grow-1"></div> - <!-- space --> - <div class="d-flex"> - <span class="d-flex align-items-center"> - Voxel space - </span> + <button + mat-icon-button + (click)="resetNavigation({position:true})" + matTooltip="Reset position"> + <i class="iavic iavic-translation"></i> + </button> - <mat-slide-toggle - [formControl]="statusPanelFormCtrl" - class="pl-2 pr-2"> - </mat-slide-toggle> + <button + mat-icon-button + (click)="resetNavigation({rotation:true})" + matTooltip="Reset rotation"> + <i class="iavic iavic-rotation"></i> + </button> - <span class="d-flex align-items center"> - Physical space - </span> - </div> + <button + mat-icon-button + (click)="resetNavigation({zoom:true})" + matTooltip="Reset zoom"> + <i class="iavic iavic-scaling"></i> + </button> - <!-- coord --> - <div class="d-flex"> + <mat-divider [vertical]="true"></mat-divider> - <mat-form-field class="flex-grow-1"> + <button mat-icon-button + [attr.aria-label]="HIDE_FULL_STATUS_PANEL_ARIA_LABEL" + (click)="statusCardQT.ngOnChanges(); showFull = false"> + <i class="fas fa-angle-up"></i> + </button> + </div> + + <!-- space --> + <div class="d-flex"> + <span class="d-flex align-items-center"> + Voxel space + </span> + + <mat-slide-toggle + [formControl]="statusPanelFormCtrl" + class="pl-2 pr-2"> + </mat-slide-toggle> + + <span class="d-flex align-items center"> + Physical space + </span> + </div> + + <!-- coord --> + <div class="d-flex"> + + <mat-form-field class="flex-grow-1"> + <mat-label> + {{ (statusPanelRealSpace$ | async) ? 'Physical Coord' : 'Voxel Coord' }} + </mat-label> + <input type="text" + matInput + (keydown.enter)="textNavigateTo(navInput.value)" + (keydown.tab)="textNavigateTo(navInput.value)" + [value]="navVal$ | async" + #navInput="matInput"> + + </mat-form-field> + + <div class="w-0 position-relative"> + <button + (click)="showBottomSheet(shareTmpl)" + [attr.aria-label]="SHARE_BTN_ARIA_LABEL" + mat-icon-button + class="position-absolute share-btn"> + <i class="fas fa-share-square"></i> + </button> + </div> + </div> + + <!-- cursor pos --> + <mat-form-field *ngIf="!(useTouchInterface$ | async)" + class="w-100"> <mat-label> - {{ (statusPanelRealSpace$ | async) ? 'Physical Coord' : 'Voxel Coord' }} + Cursor Position </mat-label> <input type="text" matInput - (keydown.enter)="textNavigateTo(navInput.value)" - (keydown.tab)="textNavigateTo(navInput.value)" - [value]="navVal$ | async" - #navInput="matInput"> - + [readonly]="true" + [value]="mouseVal$ | async"> </mat-form-field> - <div class="w-0 position-relative"> - <button - (click)="showBottomSheet(shareTmpl)" - [attr.aria-label]="SHARE_BTN_ARIA_LABEL" - mat-icon-button - class="position-absolute share-btn"> - <i class="fas fa-share-square"></i> - </button> - </div> - </div> - - <!-- cursor pos --> - <mat-form-field *ngIf="!(useTouchInterface$ | async)" - class="w-100"> - <mat-label> - Cursor Position - </mat-label> - <input type="text" - matInput - [readonly]="true" - [value]="mouseVal$ | async"> - </mat-form-field> - - </mat-card-content> -</mat-card> + </mat-card-content> + </mat-card> </div> <!-- minimised status bar --> @@ -130,7 +130,7 @@ <button mat-icon-button [attr.aria-label]="SHOW_FULL_STATUS_PANEL_ARIA_LABEL" - (click)="showFull = true; quickTourData.position = quickTourExpandedPosition"> + (click)="statusCardQT.ngOnChanges(); showFull = true"> <i class="fas fa-angle-down"></i> </button> </div> diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 49a5d3b10..36e64eb45 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,6 +1,6 @@ -import {AfterViewInit, Component, Inject, Input, OnDestroy, Optional, ViewChild} from "@angular/core"; +import { Component, Inject, Input, OnDestroy, Optional, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import {BehaviorSubject, combineLatest, Observable, of, Subject, Subscription} from "rxjs"; +import { combineLatest, Observable, of, Subject, Subscription } from "rxjs"; import { distinctUntilChanged, filter, map, startWith } from "rxjs/operators"; import { viewerStateHelperSelectParcellationWithId, viewerStateRemoveAdditionalLayer, viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; import { viewerStateContextedSelectedRegionsSelector, viewerStateGetOverlayingAdditionalParcellations, viewerStateParcVersionSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector, viewerStateStandAloneVolumes } from "src/services/state/viewerState/selectors" @@ -12,7 +12,7 @@ import { REGION_OF_INTEREST } from "src/util/interfaces"; import { animate, state, style, transition, trigger } from "@angular/animations"; import { SwitchDirective } from "src/util/directives/switch.directive"; import { TSupportedViewer } from "../constants"; -import {QuickTourData, QuickTourPosition} from "src/ui/quickTour/constrants"; +import { IQuickTourData } from "src/ui/quickTour/constrants"; @Component({ selector: 'iav-cmp-viewer-container', @@ -78,22 +78,18 @@ export class ViewerCmp implements OnDestroy { @ViewChild('sideNavFullLeftSwitch', { static: true }) private sidenavLeftSwitch: SwitchDirective - public quickTourRegionSearchCollapsedPosition: QuickTourPosition = {arrow: 'arrow5', left: 30, top: 60, arrowPosition: 'left', arrowAlign: 'top', arrowMargin: {top: -20}, arrowTransform: 'rotate(25deg)'} - public quickTourRegionSearchExpandedPosition: QuickTourPosition = {left: 30, top: 70, arrowPosition: 'top', arrowAlign: 'center'} - public quickTourRegionSearch: QuickTourData = { + + public quickTourRegionSearch: IQuickTourData = { order: 7, description: QUICKTOUR_DESC.REGION_SEARCH, - position: this.quickTourRegionSearchCollapsedPosition } - public quickTourAtlasSelector: QuickTourData = { + public quickTourAtlasSelector: IQuickTourData = { order: 0, description: QUICKTOUR_DESC.ATLAS_SELECTOR, - position: {position: 'bottom', align: 'right', arrowPosition: 'top', arrowAlign: 'center'} } - public quickTourChips: QuickTourData = { + public quickTourChips: IQuickTourData = { order: 5, description: QUICKTOUR_DESC.CHIPS, - position: {position: 'top', align: 'center', arrowPosition: 'bottom', arrowAlign: 'center', arrowTransform: 'scaleX(-1) rotate(180deg)'} } @@ -229,7 +225,6 @@ export class ViewerCmp implements OnDestroy { } private openSideNavs() { - this.quickTourRegionSearch.position = this.quickTourRegionSearchExpandedPosition this.sidenavLeftSwitch && this.sidenavLeftSwitch.open() this.sidenavTopSwitch && this.sidenavTopSwitch.open() } diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 725d50a5f..ea30a1e28 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -9,16 +9,13 @@ <mat-drawer-container [iav-switch-initstate]="false" iav-switch - (iav-switch-event)="quickTourRegionSearch.position = - sideNavTopSwitch.switchState? quickTourRegionSearchExpandedPosition - : quickTourRegionSearchCollapsedPosition" #sideNavTopSwitch="iavSwitch" class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" [hasBackdrop]="false"> <!-- sidenav-content --> - <!-- (closedStart)="sideNavFullLeftSwitch.switchState && matDrawerLeft.close()" + <!-- (closedStart)="sideNavwFullLeftSwitch.switchState && matDrawerLeft.close()" (openedStart)="sideNavFullLeftSwitch.switchState && matDrawerLeft.open()" --> <mat-drawer class="box-shadow-none border-0 pe-none bg-none col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2" mode="side" @@ -29,10 +26,11 @@ #matDrawerTop="matDrawer"> <div class="h-0 w-100 region-text-search-autocomplete-position" - quick-tour - [quick-tour-description]="quickTourRegionSearch.description" - [quick-tour-order]="quickTourRegionSearch.order" - [quick-tour-position]="quickTourRegionSearch.position"> + *ngIf="matDrawerTop.opened"> + + <!-- quick-tour + [quick-tour-description]="quickTourRegionSearch.description" + [quick-tour-order]="quickTourRegionSearch.order" --> <ng-container *ngTemplateOutlet="autocompleteTmpl"> </ng-container> </div> @@ -101,8 +99,7 @@ class="iv-custom-comp bg card m-2 mat-elevation-z2" quick-tour [quick-tour-description]="quickTourAtlasSelector.description" - [quick-tour-order]="quickTourAtlasSelector.order" - [quick-tour-position]="quickTourAtlasSelector.position"> + [quick-tour-order]="quickTourAtlasSelector.order"> <atlas-dropdown-selector class="pe-all mt-2"> </atlas-dropdown-selector> </div> @@ -210,10 +207,9 @@ <div class="flex-grow-1 flex-shrink-1 overflow-x-auto"> <mat-chip-list class="d-inline-block" - quick-tour - [quick-tour-description]="quickTourChips.description" - [quick-tour-order]="quickTourChips.order" - [quick-tour-position]="quickTourChips.position"> + quick-tour + [quick-tour-description]="quickTourChips.description" + [quick-tour-order]="quickTourChips.order"> <!-- additional layer --> <ng-container> -- GitLab