diff --git a/common/constants.js b/common/constants.js index d827b1361799462953c28e29df04278fcb527467..97fcd1559c745356cb46e0739791bb79e5c5dc5c 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 eccfa00f6924ae8f30f03f7b7f4f219263607a67..86aaff323552bd05d79edd6d0be537e23ddd57b3 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 da89134827cbdbeef9b0a75c69ebd6fe73c7ab31..2676a48d52a76eb8c071a3b61943d55393178d2e 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 0000000000000000000000000000000000000000..00eeb18062afc4e9ac4eb45decef7773000ad9dd --- /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 0000000000000000000000000000000000000000..f4355c89cc2fe93a4e2ca5928f97f73859580451 --- /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 0000000000000000000000000000000000000000..39281bdffc1e34b6347a9deeeaf567403e764a65 --- /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 2e4b8c2f3c9a59032b4591d54017fe0060cbdc24..45e986f6619a99a4c2466f133c2a2fdd6d3dba9a 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 d34eb0bb2ecdc3ade0328836d652b2ec7166fee7..f7f0d4c08dccca58a92687be9f0520649ab2a6b5 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 318c8d0c206ef85cb9c5f125597d80a0e0c6ca80..0990846745cf6a99ddca9ad9dd4e65c8e9a44bdc 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 db3460a596b28414d3af369f6fe6050c2519e7cb..9b9cd07398d137c1d3cdf53a1164947b2b710a12 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 0000000000000000000000000000000000000000..60a2f2d1f9a00c9b9b1c4649579fd9cf3c61c8c4 --- /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 0000000000000000000000000000000000000000..c6d9f7bc0c7df887ee87b0114e6b37c18187f437 --- /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 0000000000000000000000000000000000000000..af727722186a62518f73be4105ae5311a3ad0095 --- /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 bda24c7a9a1460129005b897640e88bed89ab2ba..32054b1a5d69689624253f9f3bfd6a7b36235e6b 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 648fe1f12ac0bcf361691662e5bd2b40ba41b4dc..0000000000000000000000000000000000000000 --- 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 041c3bde2f6f7f9cd9a4cf0fb318a8c1d80ec522..0000000000000000000000000000000000000000 --- 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 d78a10c98a257d5a2548f696c02d90d9508a969c..0000000000000000000000000000000000000000 --- 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 0dfa246f1dba8ae77fab533106a9a42dacd1d9af..a15328676e0e0504285ed0d46d1d4b80b51fceac 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 8b4f55c35f67fc49e0038bf819278f28f6fe6efd..aee22de8150058eb32caefa04d87ddc1898331e4 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 0000000000000000000000000000000000000000..2d57658f750851c5d708ed0a2962d9d0a3b43c77 --- /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 25e810ea70b989a4e67ea70ae107fb598444037e..eeb79967a2de60a22bcd7d70fe13b24e9a3d8fb6 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 0000000000000000000000000000000000000000..cd22fb2f4a0c9a9fa9687f14ebe94cfbf5b0f671 --- /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 0000000000000000000000000000000000000000..61392d6f4ea9770288d253264095ad63cff1d96c --- /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 0000000000000000000000000000000000000000..ce8de31804fcaec53296925c5eef5235979719b0 --- /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 0000000000000000000000000000000000000000..f50c7df0e929a935b56176311b887d585d6412eb --- /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 52617785c062aace265a6392c3e4f8d133abd483..fe162efc308dcec3e1d3ad81770f110acb30caac 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 2e09e2117ab5bcba3ef4c0082ad6a574fb248337..39a33d414f2514450a8f541ca24413cf5e5f4f8a 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 13990f16772ecfb573f72ad17744dfc34d98ab45..9108b13790db2fb6a958c1a556b3f864615d9884 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 4691b93cde728b73d7a1a587d54643f47fe107e0..c0cc7d57ebd8407e9f100d921f8115a6c1f216fc 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 d3639b7dadc54f35fa988f81e681a22e61701622..5e4d793a725e31f41f5bd45406a11b95ef1340a6 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 49a5d3b10d856771a07d9f0d1e2a25e567f55251..36e64eb45989d9eb2289d9394d40101bb1b762cb 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 725d50a5fd5bad7a940c19447cece50821f3a70f..ea30a1e28e8e71d02dc1396ad9d120d392356928 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>