diff --git a/common/constants.js b/common/constants.js index b37adb9a66e77d60f340b0a8cb2247136ea2ae4e..3a63c22ab349feba1dc0be8e2fdee72486376464 100644 --- a/common/constants.js +++ b/common/constants.js @@ -6,6 +6,7 @@ OPEN: 'Open', EXPAND: 'Expand', COLLAPSE: 'Collapse', + COPY_TO_CLIPBOARD: 'Copy to clipboard', // dataset specific EXPLORE_DATASET_IN_KG: `Explore dataset in Knowledge Graph`, diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts index 59275a07d0ace42729e7864dd4e9d8a66c668510..2ffb0a698f1adc4d4d4d133c40a6da36b665fbf2 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts @@ -29,7 +29,7 @@ export class AnnotationList { @ViewChild(FileInputDirective) fileInput: FileInputDirective - public managedAnnotations$ = this.annotSvc.managedAnnotations$ + public managedAnnotations$ = this.annotSvc.spaceFilteredManagedAnnotations$ public manAnnExists$ = this.managedAnnotations$.pipe( map(arr => !!arr && arr.length > 0), diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html index 4cd227d10dd4d9a5db7a2e9f6b91f8427c4da849..17ac890eafc6f4049b039ad7c36147a2d481642d 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html @@ -31,7 +31,7 @@ [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT" [matTooltip]="ARIA_LABELS.USER_ANNOTATION_EXPORT" [disabled]="!(manAnnExists$ | async)"> - <i class="fas fa-file-export"></i> + <i class="fas fa-download"></i> </button> </mat-card-subtitle> </div> diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts index a2314a4efb2b54c097e65f1d52b86b6c87d9fa3d..0bacfe6b2d2d0f92d2b292429dcc4682bd05f1f3 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts @@ -6,6 +6,7 @@ import { ARIA_LABELS } from 'common/constants' import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, CONTEXT_MENU_ITEM_INJECTOR, TContextMenu } from "src/util"; import { TContextArg } from "src/viewerModule/viewer.interface"; import { TContextMenuReg } from "src/contextMenuModule"; +import { MatSnackBar } from "@angular/material/snack-bar"; @Component({ selector: 'annotating-tools-panel', @@ -29,6 +30,7 @@ export class AnnotationMode implements OnDestroy{ constructor( private store$: Store<any>, private modularToolSvc: ModularUserAnnotationToolService, + snackbar: MatSnackBar, @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, @Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu<TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>>> ) { @@ -45,6 +47,13 @@ export class AnnotationMode implements OnDestroy{ register(stopClickProp) this.onDestroyCb.push(() => deregister(stopClickProp)) } + + this.modularToolSvc.loadStoredAnnotations() + .catch(e => { + snackbar.open(`Loading annotations from storage failed: ${e.toString()}`, 'Dismiss', { + duration: 3000 + }) + }) } exitAnnotationMode(){ diff --git a/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts b/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts index 0aeb8ac797f3705accf63f687e832c6969f5f129..1109fbdbc342124d842fe82b59ee02c2a51d0de4 100644 --- a/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts +++ b/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts @@ -1,110 +1,53 @@ -import { Directive, HostListener, Inject, OnDestroy, Optional } from "@angular/core"; +import { Directive, HostListener, Inject, Input, Optional } from "@angular/core"; import { viewerStateSetViewerMode } from "src/services/state/viewerState/actions"; import { ARIA_LABELS } from "common/constants"; -import { Store } from "@ngrx/store"; +import { select, Store } from "@ngrx/store"; import { TContextArg } from "src/viewerModule/viewer.interface"; import { TContextMenuReg } from "src/contextMenuModule"; import { CONTEXT_MENU_ITEM_INJECTOR, TContextMenu } from "src/util"; import { ModularUserAnnotationToolService } from "../tools/service"; -import { IAnnotationGeometry } from "../tools/type"; -import { retry } from 'common/util' -import { MatSnackBar } from "@angular/material/snack-bar"; +import { Subscription } from "rxjs"; +import { viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors"; @Directive({ selector: '[annotation-switch]' }) -export class AnnotationSwitch implements OnDestroy{ - - private onDestroyCb: Function[] = [] +export class AnnotationSwitch { + @Input('annotation-switch-mode') + mode: 'toggle' | 'off' | 'on' = 'on' + + private currMode = null + private subs: Subscription[] = [] constructor( private store$: Store<any>, private svc: ModularUserAnnotationToolService, - private snackbar: MatSnackBar, @Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu<TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>>> ) { - - const sub = this.svc.managedAnnotations$.subscribe(manAnn => this.manangedAnnotations = manAnn) - this.onDestroyCb.push( - () => sub.unsubscribe() + this.subs.push( + this.store$.pipe( + select(viewerStateViewerModeSelector) + ).subscribe(mode => { + this.currMode = mode + }) ) - - - const loadAnn = async () => { - try { - const anns = await this.getAnnotation() - for (const ann of anns) { - this.svc.importAnnotation(ann) - } - } catch (e) { - this.snackbar.open(`Error loading annotation from storage: ${e.toString()}`, 'Dismiss', { - duration: 3000 - }) - } - } - loadAnn() - } - - ngOnDestroy(){ - while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() - } - - /** - * TODO move annotation storage/retrival to more logical location - */ - @HostListener('window:beforeunload') - onPageHide(){ - this.storeAnnotation(this.manangedAnnotations) } @HostListener('click') onClick() { - this.store$.dispatch( - viewerStateSetViewerMode({ - payload: ARIA_LABELS.VIEWER_MODE_ANNOTATING - }) - ) - } - - private manangedAnnotations = [] - private localstoragekey = 'userAnnotationKey' - private storeAnnotation(anns: IAnnotationGeometry[]){ - const arr = [] - for (const ann of anns) { - const json = ann.toJSON() - arr.push(json) + let payload = null + if (this.mode === 'on') payload = ARIA_LABELS.VIEWER_MODE_ANNOTATING + if (this.mode === 'off') { + if (this.currMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING) payload = null + else return } - const stringifiedJSON = JSON.stringify(arr) - const { pako } = (window as any).export_nehuba - const compressed = pako.deflate(stringifiedJSON) - let out = '' - for (const num of compressed) { - out += String.fromCharCode(num) + if (this.mode === 'toggle') { + payload = this.currMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING + ? null + : ARIA_LABELS.VIEWER_MODE_ANNOTATING } - const encoded = btoa(out) - window.localStorage.setItem(this.localstoragekey, encoded) - } - private async getAnnotation(): Promise<IAnnotationGeometry[]>{ - const encoded = window.localStorage.getItem(this.localstoragekey) - if (!encoded) return [] - const bin = atob(encoded) - - await retry(() => { - if (!!(window as any).export_nehuba) return true - else throw new Error(`export nehuba not yet ready`) - }, { - timeout: 1000, - retries: 10 - }) - - const { pako } = (window as any).export_nehuba - const decoded = pako.inflate(bin, { to: 'string' }) - const arr = JSON.parse(decoded) - const out: IAnnotationGeometry[] = [] - for (const obj of arr) { - const geometry = this.svc.parseAnnotationObject(obj) - out.push(geometry) - } - return out + this.store$.dispatch( + viewerStateSetViewerMode({ payload }) + ) } } diff --git a/src/atlasComponents/userAnnotations/tools/delete.ts b/src/atlasComponents/userAnnotations/tools/delete.ts index ecf04a10d3e7dabed9b5094fe5cb59443fde0c3a..63a73c57816ffc9a2e5dce69459bbe4f74bf681b 100644 --- a/src/atlasComponents/userAnnotations/tools/delete.ts +++ b/src/atlasComponents/userAnnotations/tools/delete.ts @@ -23,7 +23,7 @@ export class ToolDelete extends AbsToolClass<Point> implements IAnnotationTools, managedAnnotations$ = new Subject<Point[]>() private allManAnnotations: IAnnotationGeometry[] = [] - allNgAnnotations$ = new Subject<TNgAnnotationPoint[]>() + constructor( annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>>, callback: TCallbackFunction diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts index c1ac7f8a4ce335a863e4b572c1b8774b51fee6d0..f48247a65abe28e6c92cf334cf1ec85cb4c81b6e 100644 --- a/src/atlasComponents/userAnnotations/tools/line.ts +++ b/src/atlasComponents/userAnnotations/tools/line.ts @@ -13,7 +13,7 @@ import { } from "./type"; import { Point, TPointJsonSpec } from './point' import { OnDestroy } from "@angular/core"; -import { merge, Observable, Subject, Subscription } from "rxjs"; +import { Observable, Subject, Subscription } from "rxjs"; import { filter, switchMapTo, takeUntil } from "rxjs/operators"; import { getUuid } from "src/util/fn"; @@ -192,11 +192,8 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On subs: Subscription[] = [] - private forceRefreshAnnotations$ = new Subject() private managedAnnotations: Line[] = [] public managedAnnotations$ = new Subject<Line[]>() - public allNgAnnotations$ = new Subject<INgAnnotationTypes[keyof INgAnnotationTypes][]>() - onMouseMoveRenderPreview(pos: [number, number, number]) { if (this.selectedLine && !!this.selectedLine.points[0]) { @@ -259,16 +256,14 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On ).subscribe(mouseev => { const crd = mouseev.detail.ngMouseEvent if (!this.selectedLine) { - this.selectedLine = new Line({ + const newLine = new Line({ space: this.space, "@type": 'siibra-ex/annotation/line', points: [] }) - const { id } = this.selectedLine - this.selectedLine.remove = () => this.removeAnnotation(id) - this.selectedLine.addLinePoints(crd) - this.managedAnnotations.push(this.selectedLine) - this.managedAnnotations$.next(this.managedAnnotations) + newLine.addLinePoints(crd) + this.addAnnotation(newLine) + this.selectedLine = newLine } else { this.selectedLine.addLinePoints(crd) @@ -280,24 +275,6 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On } }), - /** - * conditions by which ng annotations are refreshed - */ - merge( - toolDeselect$, - toolSelThenClick$, - this.forceRefreshAnnotations$, - ).pipe( - - ).subscribe(() => { - let out: INgAnnotationTypes['line'][] = [] - for (const managedAnn of this.managedAnnotations) { - out = out.concat(...managedAnn.toNgAnnotation()) - } - - this.allNgAnnotations$.next(out) - }), - /** * emit on init, and reset on mouseup$ * otherwise, pairwise confuses last drag event and first drag event @@ -319,7 +296,7 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On } else { annotation.translate(deltaX, deltaY, deltaZ) } - this.forceRefreshAnnotations$.next(null) + this.managedAnnotations$.next(this.managedAnnotations) }) ) } @@ -331,9 +308,9 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On addAnnotation(line: Line) { const idx = this.managedAnnotations.findIndex(ann => ann.id === line.id) if (idx >= 0) throw new Error(`Line annotation has already been added`) + line.remove = () => this.removeAnnotation(line.id) this.managedAnnotations.push(line) this.managedAnnotations$.next(this.managedAnnotations) - this.forceRefreshAnnotations$.next(null) } removeAnnotation(id: string){ @@ -343,7 +320,6 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On } this.managedAnnotations.splice(idx, 1) this.managedAnnotations$.next(this.managedAnnotations) - this.forceRefreshAnnotations$.next(null) } } diff --git a/src/atlasComponents/userAnnotations/tools/line/line.component.ts b/src/atlasComponents/userAnnotations/tools/line/line.component.ts index 82289d20a86efdd15d98c649d807dd4436d7fa72..bf3d32401fc9cfc2378b2a4d17000e53cef2390d 100644 --- a/src/atlasComponents/userAnnotations/tools/line/line.component.ts +++ b/src/atlasComponents/userAnnotations/tools/line/line.component.ts @@ -4,7 +4,6 @@ import { Store } from "@ngrx/store"; import { Line, LINE_ICON_CLASS } from "../line"; import { ToolCmpBase } from "../toolCmp.base"; import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; -import { Clipboard } from "@angular/cdk/clipboard"; import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; import { Point } from "../point"; import { ARIA_LABELS } from 'common/constants' @@ -30,12 +29,11 @@ export class LineUpdateCmp extends ToolCmpBase implements OnDestroy{ constructor( private store: Store<any>, - snackbar: MatSnackBar, - clipboard: Clipboard, + private snackbar: MatSnackBar, cStore: ComponentStore<{ useFormat: TExportFormats }>, @Optional() @Inject(UDPATE_ANNOTATION_TOKEN) updateAnnotation: IAnnotationGeometry, ){ - super(clipboard, snackbar, cStore) + super(cStore) if (updateAnnotation) { if (updateAnnotation instanceof Line) { diff --git a/src/atlasComponents/userAnnotations/tools/line/line.template.html b/src/atlasComponents/userAnnotations/tools/line/line.template.html index 7b05ff79b4780f4d6858ec4ec2b6e51683fbd727..e0b3b06a78c4ebb58e26e4d9254f6c59e74a2ca3 100644 --- a/src/atlasComponents/userAnnotations/tools/line/line.template.html +++ b/src/atlasComponents/userAnnotations/tools/line/line.template.html @@ -40,25 +40,12 @@ iav-stop="click"> <div class="iv-custom-comp text"> - <mat-form-field> - <mat-label> - {{ useFormat }} - </mat-label> - <textarea - disabled="true" - matInput - #exportTarget>{{ updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat }}</textarea> - - <button mat-icon-button - matSuffix - iav-stop="click" - aria-label="Copy to clipboard" - matTooltip="Copy to clipboard." - (click)="copyToClipboard(exportTarget.value)" - color="basic"> - <i class="fas fa-copy"></i> - </button> - </mat-form-field> + <textarea-copy-export + [textarea-copy-export-label]="useFormat" + [textarea-copy-export-text]="updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat" + [textarea-copy-export-download-filename]="updateAnnotation.id + '.sands.json'" + [textarea-copy-export-disable]="true"> + </textarea-copy-export> </div> </div> </mat-menu> diff --git a/src/atlasComponents/userAnnotations/tools/module.ts b/src/atlasComponents/userAnnotations/tools/module.ts index e7a3a040231449c58180cf3f3aef9b961377b20b..869c307409d53b76d9b409954d32a0acca2f5341 100644 --- a/src/atlasComponents/userAnnotations/tools/module.ts +++ b/src/atlasComponents/userAnnotations/tools/module.ts @@ -8,25 +8,28 @@ import { PointUpdateCmp } from "./point/point.component"; import { PolyUpdateCmp } from "./poly/poly.component"; import { ModularUserAnnotationToolService } from "./service"; import { ToFormattedStringPipe } from "./toFormattedString.pipe"; -import { ANNOTATION_EVENT_INJ_TOKEN, } from "./type"; +import { ANNOTATION_EVENT_INJ_TOKEN } from "./type"; import { Line, ToolLine } from "src/atlasComponents/userAnnotations/tools/line"; - import { Point, ToolPoint } from "./point"; import { ToolSelect } from "./select"; import { ToolDelete } from "./delete"; import { Polygon, ToolPolygon } from "./poly"; +import { ZipFilesOutputModule } from "src/zipFilesOutput/module"; +import { TextareaCopyExportCmp } from "./textareaCopyExport/textareaCopyExport.component"; @NgModule({ imports: [ CommonModule, AngularMaterialModule, UtilModule, + ZipFilesOutputModule, ], declarations: [ LineUpdateCmp, PolyUpdateCmp, PointUpdateCmp, ToFormattedStringPipe, + TextareaCopyExportCmp, ], exports: [ LineUpdateCmp, @@ -44,7 +47,9 @@ import { Polygon, ToolPolygon } from "./poly"; export class UserAnnotationToolModule { - constructor(svc: ModularUserAnnotationToolService){ + constructor( + svc: ModularUserAnnotationToolService, + ){ const selTool = svc.registerTool({ toolCls: ToolSelect }) diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts index 233413921fd8b1317f098db99e02dd00023a98b1..7ea3b5f55ecc64e0ac20e7b7a9cded0502a1ba80 100644 --- a/src/atlasComponents/userAnnotations/tools/point.ts +++ b/src/atlasComponents/userAnnotations/tools/point.ts @@ -110,8 +110,6 @@ export class ToolPoint extends AbsToolClass<Point> implements IAnnotationTools, public subs: Subscription[] = [] private managedAnnotations: Point[] = [] public managedAnnotations$ = new Subject<Point[]>() - public allNgAnnotations$ = new Subject<INgAnnotationTypes[keyof INgAnnotationTypes][]>() - private forceRefresh$ = new Subject() constructor( annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>>, @@ -142,15 +140,8 @@ export class ToolPoint extends AbsToolClass<Point> implements IAnnotationTools, space, '@type': 'siibra-ex/annotation/point' }) - const { id } = pt - pt.remove = () => this.removeAnnotation(id) - this.managedAnnotations.push(pt) - this.managedAnnotations$.next(this.managedAnnotations) + this.addAnnotation(pt) - /** - * force refresh of ng annotation - */ - this.forceRefresh$.next(null) /** * deselect on selecting a point */ @@ -166,32 +157,18 @@ export class ToolPoint extends AbsToolClass<Point> implements IAnnotationTools, const foundAnn = this.managedAnnotations.find(ann => ann.id === pickedAnnotationId) if (foundAnn) { foundAnn.translate(deltaX, deltaY, deltaZ) - this.forceRefresh$.next(null) + this.managedAnnotations$.next(this.managedAnnotations) } }), - /** - * evts which forces redraw of ng annotations - */ - merge( - this.forceRefresh$, - ).subscribe(() => { - let out: INgAnnotationTypes['point'][] = [] - for (const managedAnn of this.managedAnnotations) { - if (managedAnn.space['@id'] === this.space['@id']) { - out = out.concat(...managedAnn.toNgAnnotation()) - } - } - this.allNgAnnotations$.next(out) - }) ) } addAnnotation(point: Point){ const found = this.managedAnnotations.find(p => p.id === point.id) if (found) throw new Error(`Point annotation already added`) + point.remove = () => this.removeAnnotation(point.id) this.managedAnnotations.push(point) this.managedAnnotations$.next(this.managedAnnotations) - this.forceRefresh$.next(null) } /** @@ -206,7 +183,6 @@ export class ToolPoint extends AbsToolClass<Point> implements IAnnotationTools, } this.managedAnnotations.splice(idx, 1) this.managedAnnotations$.next(this.managedAnnotations) - this.forceRefresh$.next(null) } onMouseMoveRenderPreview(pos: [number, number, number]) { diff --git a/src/atlasComponents/userAnnotations/tools/point/point.component.ts b/src/atlasComponents/userAnnotations/tools/point/point.component.ts index 8dd44c0d7ed9c6b8e8d42310fbb0b99b3fa252d4..c28152125a1a006ac1f69d8543792364dcb8b4ab 100644 --- a/src/atlasComponents/userAnnotations/tools/point/point.component.ts +++ b/src/atlasComponents/userAnnotations/tools/point/point.component.ts @@ -1,8 +1,6 @@ import { Component, ElementRef, Inject, Input, OnDestroy, Optional, ViewChild } from "@angular/core"; -import { MatSnackBar } from "@angular/material/snack-bar"; import { Point, POINT_ICON_CLASS } from "../point"; import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; -import { Clipboard } from "@angular/cdk/clipboard"; import { ToolCmpBase } from "../toolCmp.base"; import { Store } from "@ngrx/store"; import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; @@ -30,12 +28,10 @@ export class PointUpdateCmp extends ToolCmpBase implements OnDestroy{ constructor( private store: Store<any>, - snackbar: MatSnackBar, - clipboard: Clipboard, cStore: ComponentStore<{ useFormat: TExportFormats }>, @Optional() @Inject(UDPATE_ANNOTATION_TOKEN) updateAnnotation: IAnnotationGeometry, ){ - super(clipboard, snackbar, cStore) + super(cStore) if (updateAnnotation) { if (updateAnnotation instanceof Point) { diff --git a/src/atlasComponents/userAnnotations/tools/point/point.template.html b/src/atlasComponents/userAnnotations/tools/point/point.template.html index 2eeccd57ca5afa1f41e15e20be332b9f6c90a274..b1b5ab285e11bc2926f3a00d4b8b0b39b4ab0579 100644 --- a/src/atlasComponents/userAnnotations/tools/point/point.template.html +++ b/src/atlasComponents/userAnnotations/tools/point/point.template.html @@ -39,25 +39,13 @@ iav-stop="click"> <div class="iv-custom-comp text"> - <mat-form-field> - <mat-label> - {{ useFormat }} - </mat-label> - <textarea - disabled="true" - matInput - #exportTarget>{{ updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat }}</textarea> - - <button mat-icon-button - matSuffix - iav-stop="click" - aria-label="Copy to clipboard" - matTooltip="Copy to clipboard." - (click)="copyToClipboard(exportTarget.value)" - color="basic"> - <i class="fas fa-copy"></i> - </button> - </mat-form-field> + + <textarea-copy-export + [textarea-copy-export-label]="useFormat" + [textarea-copy-export-text]="updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat" + [textarea-copy-export-download-filename]="updateAnnotation.id + '.sands.json'" + [textarea-copy-export-disable]="true"> + </textarea-copy-export> </div> </div> </mat-menu> diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts index 4dc49ee3068ae4ce30b26b34a1d69f3cb038f927..c0a65bca5b81b91eb6e918b5c6fd13f7565ce475 100644 --- a/src/atlasComponents/userAnnotations/tools/poly.ts +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -237,8 +237,6 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo public managedAnnotations$ = new Subject<Polygon[]>() public subs: Subscription[] = [] - private forceRefreshAnnotations$ = new Subject() - public allNgAnnotations$ = new Subject<INgAnnotationTypes[keyof INgAnnotationTypes][]>() onMouseMoveRenderPreview(pos: [number, number, number]) { if (this.lastAddedPoint) { @@ -312,6 +310,8 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo } } + this.managedAnnotations$.next(this.managedAnnotations) + this.selectedPoly = null this.lastAddedPoint = null }), @@ -324,15 +324,14 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo withLatestFrom(this.hoverAnnotation$) ).subscribe(([mouseev, ann]) => { if (!this.selectedPoly) { - this.selectedPoly = new Polygon({ + const newPoly = new Polygon({ edges: [], points: [], space: this.space, '@type': 'siibra-ex/annotation/polyline' }) - const { id } = this.selectedPoly - this.selectedPoly.remove = () => this.removeAnnotation(id) - this.managedAnnotations.push(this.selectedPoly) + this.addAnnotation(newPoly) + this.selectedPoly = newPoly } else { if (ann.detail) { @@ -365,28 +364,6 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo this.managedAnnotations$.next(this.managedAnnotations) }), - /** - * conditions by which ng annotations are refreshed - */ - merge( - toolDeselect$, - toolSelThenClick$, - this.forceRefreshAnnotations$, - ).pipe( - - ).subscribe(() => { - let out: INgAnnotationTypes['line'][] = [] - for (const managedAnn of this.managedAnnotations) { - /** - * only emit annotations in matching space - */ - if (managedAnn.space["@id"] === this.space["@id"]) { - out = out.concat(...managedAnn.toNgAnnotation()) - } - } - this.allNgAnnotations$.next(out) - }), - /** * translate point when on hover a point * translate entire annotation when hover edge @@ -412,7 +389,7 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo parsedAnnotation.point.translate(deltaX, deltaY, deltaZ) } - this.forceRefreshAnnotations$.next(null) + this.managedAnnotations$.next(this.managedAnnotations) }), ) } @@ -420,9 +397,9 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo addAnnotation(poly: Polygon){ const idx = this.managedAnnotations.findIndex(ann => ann.id === poly.id) if (idx >= 0) throw new Error(`Polygon already added.`) + poly.remove = () => this.removeAnnotation(poly.id) this.managedAnnotations.push(poly) this.managedAnnotations$.next(this.managedAnnotations) - this.forceRefreshAnnotations$.next(null) } removeAnnotation(id: string) { @@ -432,7 +409,6 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo } this.managedAnnotations.splice(idx, 1) this.managedAnnotations$.next(this.managedAnnotations) - this.forceRefreshAnnotations$.next(null) } ngOnDestroy(){ diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts b/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts index 885c8b41c2292737e681f4a156a9f93ab77631f4..9d8371b404a5eec8d4bc4ca60bbcf07cdfac2c5e 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts @@ -3,7 +3,6 @@ import { MatSnackBar } from "@angular/material/snack-bar"; import { Polygon, POLY_ICON_CLASS } from "../poly"; import { ToolCmpBase } from "../toolCmp.base"; import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; -import { Clipboard } from "@angular/cdk/clipboard"; import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; import { Store } from "@ngrx/store"; import { Point } from "../point"; @@ -32,12 +31,11 @@ export class PolyUpdateCmp extends ToolCmpBase implements OnDestroy{ constructor( private store: Store<any>, - snackbar: MatSnackBar, - clipboard: Clipboard, + private snackbar: MatSnackBar, cStore: ComponentStore<{ useFormat: TExportFormats }>, @Optional() @Inject(UDPATE_ANNOTATION_TOKEN) updateAnnotation: IAnnotationGeometry, ){ - super(clipboard, snackbar, cStore) + super(cStore) if (this.cStore) { this.sub.push( this.cStore.select(store => store.useFormat).subscribe((val: TExportFormats) => { diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html index 07fa279961fce101fff2075de6ce0eb5588d022f..ea8665437974b0c1d29b31de294b72ed426f7095 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html @@ -40,25 +40,12 @@ iav-stop="click"> <div class="iv-custom-comp text"> - <mat-form-field> - <mat-label> - {{ useFormat }} - </mat-label> - <textarea - disabled="true" - matInput - #exportTarget>{{ updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat }}</textarea> - - <button mat-icon-button - matSuffix - iav-stop="click" - aria-label="Copy to clipboard" - matTooltip="Copy to clipboard." - (click)="copyToClipboard(exportTarget.value)" - color="basic"> - <i class="fas fa-copy"></i> - </button> - </mat-form-field> + <textarea-copy-export + [textarea-copy-export-label]="useFormat" + [textarea-copy-export-text]="updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat" + [textarea-copy-export-download-filename]="updateAnnotation.id + '.sands.json'" + [textarea-copy-export-disable]="true"> + </textarea-copy-export> </div> </div> </mat-menu> diff --git a/src/atlasComponents/userAnnotations/tools/select.ts b/src/atlasComponents/userAnnotations/tools/select.ts index 2c10e4fe0a46c725a77666d5f621d3101c82a3b0..b5f614c39809580d519aef08e3270a9425536c3d 100644 --- a/src/atlasComponents/userAnnotations/tools/select.ts +++ b/src/atlasComponents/userAnnotations/tools/select.ts @@ -22,7 +22,6 @@ export class ToolSelect extends AbsToolClass<Point> implements IAnnotationTools, removeAnnotation(){} managedAnnotations$ = new Subject<Point[]>() - allNgAnnotations$ = new Subject<TNgAnnotationPoint[]>() constructor( annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>>, callback: TCallbackFunction diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index b6d4e81439796874b7478b6a617e099e44f2c3fa..5af19480ff7b1259592a24f2b1bd6c9c85449ad5 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -7,12 +7,15 @@ import { map, switchMap, filter, shareReplay, pairwise } from "rxjs/operators"; import { viewerStateSelectedTemplatePureSelector, viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors"; import { NehubaViewerUnit } from "src/viewerModule/nehuba"; import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; -import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TCallbackFunction, TSands, TGeometryJson } from "./type"; +import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TCallbackFunction, TSands, TGeometryJson, TNgAnnotationLine } from "./type"; import { switchMapWaitFor } from "src/util/fn"; import { Polygon } from "./poly"; import { Line } from "./line"; import { Point } from "./point"; +import { FilterAnnotationsBySpace } from "../filterAnnotationBySpace.pipe"; +import { retry } from 'common/util' +const LOCAL_STORAGE_KEY = 'userAnnotationKey' const IAV_VOXEL_SIZES_NM = { 'minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9': [25000, 25000, 25000], @@ -71,13 +74,11 @@ export class ModularUserAnnotationToolService implements OnDestroy{ private ngAnnotationLayer: any private activeToolName: string private forcedAnnotationRefresh$ = new BehaviorSubject(null) - private ngAnnotations$ = new Subject<{ - tool: string - annotations: INgAnnotationTypes[keyof INgAnnotationTypes][] - }>() - private selectedTmpl: any + private selectedTmpl$ = this.store.pipe( + select(viewerStateSelectedTemplatePureSelector), + ) public moduleAnnotationTypes: {instance: {name: string, iconClass: string, toolSelected$: Observable<boolean>}, onClick: Function}[] = [] private managedAnnotationsStream$ = new Subject<{ tool: string @@ -85,10 +86,22 @@ export class ModularUserAnnotationToolService implements OnDestroy{ }>() private managedAnnotations: IAnnotationGeometry[] = [] + private filterAnnotationBySpacePipe = new FilterAnnotationsBySpace() public managedAnnotations$ = this.managedAnnotationsStream$.pipe( scanCollapse(), shareReplay(1), ) + public spaceFilteredManagedAnnotations$ = combineLatest([ + this.selectedTmpl$, + this.managedAnnotations$ + ]).pipe( + map(([tmpl, annts]) => { + return this.filterAnnotationBySpacePipe.transform( + annts, + tmpl + ) + }) + ) private registeredTools: { name: string @@ -147,18 +160,8 @@ export class ModularUserAnnotationToolService implements OnDestroy{ const toolSubscriptions: Subscription[] = [] - const { allNgAnnotations$, managedAnnotations$ } = newTool + const { managedAnnotations$ } = newTool - if ( allNgAnnotations$ ) { - toolSubscriptions.push( - newTool.allNgAnnotations$.subscribe(ann => { - this.ngAnnotations$.next({ - tool: name, - annotations: ann - }) - }), - ) - } if ( managedAnnotations$ ){ toolSubscriptions.push( managedAnnotations$.subscribe(ann => { @@ -205,7 +208,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } constructor( - store: Store<any>, + private store: Store<any>, @Inject(INJ_ANNOT_TARGET) annotTarget$: Observable<HTMLElement>, @Inject(ANNOTATION_EVENT_INJ_TOKEN) private annotnEvSubj: Subject<TAnnotationEvent<keyof IAnnotationEvents>>, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Observable<NehubaViewerUnit>, @@ -372,22 +375,27 @@ export class ModularUserAnnotationToolService implements OnDestroy{ /** * on tool managed annotations update */ - const managedAnnotationUpdate$ = combineLatest([ + const spaceFilteredManagedAnnotationUpdate$ = combineLatest([ this.forcedAnnotationRefresh$, - this.ngAnnotations$.pipe( - scanCollapse(), + this.spaceFilteredManagedAnnotations$.pipe( switchMap(switchMapWaitFor({ condition: () => !!this.ngAnnotationLayer, leading: true })), ) ]).pipe( - map(([_, ngAnnos]) => ngAnnos), + map(([_, annts]) => { + const out = [] + for (const ann of annts) { + out.push(...ann.toNgAnnotation()) + } + return out + }), shareReplay(1), ) this.subscription.push( // delete removed annotations - managedAnnotationUpdate$.pipe( + spaceFilteredManagedAnnotationUpdate$.pipe( pairwise(), filter(([ oldAnn, newAnn ]) => newAnn.length < oldAnn.length), ).subscribe(([ oldAnn, newAnn ]) => { @@ -398,7 +406,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } }), //update annotations - managedAnnotationUpdate$.subscribe(arr => { + spaceFilteredManagedAnnotationUpdate$.subscribe(arr => { const ignoreNgAnnIdsSet = new Set<string>() for (const hiddenAnnot of this.hiddenAnnotations) { const ids = hiddenAnnot.getNgAnnotationIds() @@ -454,6 +462,12 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } ) this.ngAnnotationLayer = viewer.layerManager.addManagedLayer(layer) + + /** + * on template changes, the layer gets lost + * force redraw annotations if layer needs to be recreated + */ + this.forcedAnnotationRefresh$.next(null) } } else { if (this.ngAnnotationLayer) this.ngAnnotationLayer.setVisible(false) @@ -466,9 +480,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ * required for metadata in annotation geometry and voxel size */ this.subscription.push( - store.pipe( - select(viewerStateSelectedTemplatePureSelector) - ).subscribe(tmpl => { + this.selectedTmpl$.subscribe(tmpl => { this.selectedTmpl = tmpl this.annotnEvSubj.next({ type: 'metadataEv', @@ -476,9 +488,74 @@ export class ModularUserAnnotationToolService implements OnDestroy{ space: tmpl && { ['@id']: tmpl['@id'] } } }) + this.forcedAnnotationRefresh$.next(null) + }), + this.managedAnnotations$.subscribe(ann => { + this.managedAnnotations = ann }), - this.managedAnnotations$.subscribe(ann => this.managedAnnotations = ann), ) + + /** + * on window unload, save annotation + */ + + /** + * before unload, save annotations + */ + window.addEventListener('beforeunload', () => { + this.storeAnnotation(this.managedAnnotations) + }) + } + + /** + * ensure that loadStoredAnnotation only gets called once + */ + private loadFlag = false + public async loadStoredAnnotations(){ + if (this.loadFlag) return + this.loadFlag = true + + const encoded = window.localStorage.getItem(LOCAL_STORAGE_KEY) + if (!encoded) return [] + const bin = atob(encoded) + + await retry(() => { + if (!!(window as any).export_nehuba) return true + else throw new Error(`export nehuba not yet ready`) + }, { + timeout: 1000, + retries: 10 + }) + + const { pako } = (window as any).export_nehuba + const decoded = pako.inflate(bin, { to: 'string' }) + const arr = JSON.parse(decoded) + const anns: IAnnotationGeometry[] = [] + for (const obj of arr) { + const geometry = this.parseAnnotationObject(obj) + anns.push(geometry) + } + + for (const ann of anns) { + this.importAnnotation(ann) + } + } + + private storeAnnotation(anns: IAnnotationGeometry[]){ + const arr = [] + for (const ann of anns) { + const json = ann.toJSON() + arr.push(json) + } + const stringifiedJSON = JSON.stringify(arr) + const { pako } = (window as any).export_nehuba + const compressed = pako.deflate(stringifiedJSON) + let out = '' + for (const num of compressed) { + out += String.fromCharCode(num) + } + const encoded = btoa(out) + window.localStorage.setItem(LOCAL_STORAGE_KEY, encoded) } private hiddenAnnotationIds = new Set<string>() diff --git a/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.component.ts b/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a3be425dfd94693bce7f3e959001de9bfd7d2715 --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.component.ts @@ -0,0 +1,52 @@ +import { Component, Input } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { ARIA_LABELS } from 'common/constants' +import { Clipboard } from "@angular/cdk/clipboard"; + +@Component({ + selector: 'textarea-copy-export', + templateUrl: './textareaCopyExport.template.html', + styleUrls: [ + './textareaCopyExport.style.css' + ] +}) + +export class TextareaCopyExportCmp { + + @Input('textarea-copy-export-label') + label: string + + @Input('textarea-copy-export-text') + input: string + + @Input('textarea-copy-export-rows') + rows: number = 20 + + @Input('textarea-copy-export-cols') + cols: number = 50 + + + @Input('textarea-copy-export-download-filename') + filename: string = 'download.txt' + + @Input('textarea-copy-export-disable') + disableFlag: boolean = false + + public ARIA_LABELS = ARIA_LABELS + + constructor( + private snackbar: MatSnackBar, + private clipboard: Clipboard, + ){ + + } + + copyToClipboard(value: string){ + const success = this.clipboard.copy(`${value}`) + this.snackbar.open( + success ? `Copied to clipboard!` : `Failed to copy URL to clipboard!`, + null, + { duration: 1000 } + ) + } +} diff --git a/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.style.css b/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.template.html b/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.template.html new file mode 100644 index 0000000000000000000000000000000000000000..a82f199522da1981e81a01e28358937febec850e --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.template.html @@ -0,0 +1,32 @@ +<mat-form-field> + <mat-label *ngIf="label"> + {{ label }} + </mat-label> + <textarea + [rows]="rows" + [cols]="cols" + [disabled]="disableFlag" + matInput + #exportTarget>{{ input }}</textarea> + + <button mat-icon-button + matSuffix + iav-stop="click" + aria-label="Copy to clipboard" + matTooltip="Copy to clipboard." + (click)="copyToClipboard(exportTarget.value)" + color="basic"> + <i class="fas fa-copy"></i> + </button> + <button mat-icon-button + matSuffix + iav-stop="click" + [matTooltip]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" + [single-file-output]="{ + filename: filename, + filecontent: exportTarget.value + }"> + <i class="fas fa-download"></i> + </button> +</mat-form-field> diff --git a/src/atlasComponents/userAnnotations/tools/toolCmp.base.ts b/src/atlasComponents/userAnnotations/tools/toolCmp.base.ts index 586ef70c11e40c2aa43a3d04af6518d1210d8956..6eae6a303fba193dd3b5e254f3e7e56317f5f6a9 100644 --- a/src/atlasComponents/userAnnotations/tools/toolCmp.base.ts +++ b/src/atlasComponents/userAnnotations/tools/toolCmp.base.ts @@ -1,5 +1,3 @@ -import { MatSnackBar } from "@angular/material/snack-bar" -import { Clipboard } from "@angular/cdk/clipboard"; import { ARIA_LABELS } from 'common/constants' import { ComponentStore } from "src/viewerModule/componentStore"; import { TExportFormats } from "./type"; @@ -13,8 +11,6 @@ export abstract class ToolCmpBase { protected sub: Subscription[] = [] constructor( - protected clipboard: Clipboard, - protected snackbar: MatSnackBar, protected cStore: ComponentStore<{ useFormat: TExportFormats }>, ){ @@ -35,15 +31,6 @@ export abstract class ToolCmpBase { } } - copyToClipboard(value: string){ - const success = this.clipboard.copy(`${value}`) - this.snackbar.open( - success ? `Copied to clipboard!` : `Failed to copy URL to clipboard!`, - null, - { duration: 1000 } - ) - } - /** * Intention of navigating to ROI */ diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts index 2355942cc609f96d80871a44c3636285f5324581..ce9f776c15c841ea01cd0b767a0cd93b43c12cb7 100644 --- a/src/atlasComponents/userAnnotations/tools/type.ts +++ b/src/atlasComponents/userAnnotations/tools/type.ts @@ -23,11 +23,6 @@ export abstract class AbsToolClass<T extends IAnnotationGeometry> { abstract subs: Subscription[] protected space: TBaseAnnotationGeomtrySpec['space'] - /** - * @description to be overwritten by subclass. Emit the latest representation of NgAnnotations from the tool. - */ - public abstract allNgAnnotations$: Observable<INgAnnotationTypes[keyof INgAnnotationTypes][]> - /** * @description to be overwritten by subclass. Called once every mousemove event, if the tool is active. * @param {[number, number, number]} mousepos diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html index 89939c69623085628dcbdea50a19edca1bf1d3ef..55182a62ed3892abb84268c68d6b63234f07973e 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -154,6 +154,7 @@ <button mat-menu-item [disabled]="!viewerLoaded" annotation-switch + annotation-switch-mode="on" [matTooltip]="annotateTooltipText"> <mat-icon fontSet="fas" fontIcon="fa-pencil-ruler"> </mat-icon> diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 4d51060efe867aed11cb7917a1bb03e8c0560da1..b64d1bcc35e1357b8b603980de77b0e07ed09b90 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -10,7 +10,7 @@ <mat-drawer-container class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" [hasBackdrop]="false"> <mat-drawer #annotationDrawer - [mode]="'push'" + mode="side" [autoFocus]="false" [disableClose]="true" class="p-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2"> @@ -22,7 +22,7 @@ <iav-layout-fourcorners> <!-- pullable tab top right corner --> - <div iavLayoutFourCornersTopLeft class="mt-5"> + <div iavLayoutFourCornersTopLeft class="tab-toggle-container"> <div> <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { matColor: 'primary', @@ -36,6 +36,21 @@ <annotating-tools-panel class="z-index-10"> </annotating-tools-panel> </div> + + <div iavLayoutFourCornersTopRight> + <mat-card class="mat-card-sm pe-all m-4"> + <span> + Annotating + </span> + <button mat-icon-button + [matTooltip]="ARIA_LABELS.EXIT_ANNOTATION_MODE" + color="warn" + annotation-switch + annotation-switch-mode="off"> + <i class="fas fa-times"></i> + </button> + </mat-card> + </div> </iav-layout-fourcorners> diff --git a/src/zipFilesOutput/downloadSingleFile.directive.ts b/src/zipFilesOutput/downloadSingleFile.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ad93e0fac8a36c607965305291538cacb3108e7 --- /dev/null +++ b/src/zipFilesOutput/downloadSingleFile.directive.ts @@ -0,0 +1,32 @@ +import { DOCUMENT } from "@angular/common"; +import { Directive, HostListener, Inject, Input } from "@angular/core"; +import { TZipFileConfig } from "./type"; + +@Directive({ + selector: '[single-file-output]', + exportAs: 'singleFileOutput' +}) + +export class SingleFileOutput { + + @Input('single-file-output') + singleFile: TZipFileConfig + + @HostListener('click') + onClick(){ + const anchor = this.doc.createElement('a') + const blob = new Blob([this.singleFile.filecontent], { type: 'text/plain' }) + anchor.href = URL.createObjectURL(blob) + anchor.download = this.singleFile.filename + + this.doc.body.appendChild(anchor) + anchor.click() + this.doc.body.removeChild(anchor) + URL.revokeObjectURL(anchor.href) + } + constructor( + @Inject(DOCUMENT) private doc: Document + ){ + + } +} diff --git a/src/zipFilesOutput/module.ts b/src/zipFilesOutput/module.ts index 59e58e22483dfc9a2509fbee93ea5331ed60f8f2..7eeff21ca6a7c3bbb936c018ff11f2a2cd8b541a 100644 --- a/src/zipFilesOutput/module.ts +++ b/src/zipFilesOutput/module.ts @@ -1,5 +1,6 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { SingleFileOutput } from "./downloadSingleFile.directive"; import { ZipFilesOutput } from "./zipFilesOutput.directive"; @NgModule({ @@ -7,10 +8,12 @@ import { ZipFilesOutput } from "./zipFilesOutput.directive"; CommonModule, ], declarations: [ - ZipFilesOutput + ZipFilesOutput, + SingleFileOutput, ], exports: [ - ZipFilesOutput + ZipFilesOutput, + SingleFileOutput, ] })