RED-6829: update pdf event listeners to run in ng zone

This commit is contained in:
Dan Percic 2023-06-12 01:06:40 +03:00
parent f3e7811523
commit 04cc33b18e
14 changed files with 194 additions and 199 deletions

View File

@ -47,7 +47,7 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
<redaction-pdf-viewer [style.visibility]="(documentViewer.loaded$ | async) ? 'visible' : 'hidden'"></redaction-pdf-viewer> <redaction-pdf-viewer [style.visibility]="documentViewer.loaded() ? 'visible' : 'hidden'"></redaction-pdf-viewer>
<iqser-skeleton [templates]="{ dashboard: dashboardSkeleton, dossier: dossierSkeleton }"></iqser-skeleton> <iqser-skeleton [templates]="{ dashboard: dashboardSkeleton, dossier: dossierSkeleton }"></iqser-skeleton>

View File

@ -1,62 +1,58 @@
<ng-container *ngIf="componentContext$ | async as ctx"> <div class="right-title heading" translate="file-preview.tabs.document-info.label">
<div class="right-title heading" translate="file-preview.tabs.document-info.label"> <div>
<iqser-circle-button
(action)="edit()"
*ngIf="permissionsService.canEditFileAttributes(_state.file(), _state.dossier())"
[tooltip]="'file-preview.tabs.document-info.edit' | translate"
buttonId="edit-document-info-btn"
icon="iqser:edit"
tooltipPosition="before"
></iqser-circle-button>
<iqser-circle-button
(action)="documentInfoService.hide()"
[tooltip]="'file-preview.tabs.document-info.close' | translate"
buttonId="close-document-info-btn"
icon="iqser:close"
tooltipPosition="before"
></iqser-circle-button>
</div>
</div>
<div class="right-content" iqserHasScrollbar>
<div class="section">
<div *ngFor="let attr of _fileAttributes()" class="attribute">
<div class="small-label">{{ attr.label }}:</div>
<div>{{ attr.value ? (isDate(attr) ? (attr.value | date : 'd MMM yyyy') : attr.value) : '-' }}</div>
</div>
</div>
<div class="section small-label stats-subtitle">
<div> <div>
<iqser-circle-button <mat-icon svgIcon="red:folder"></mat-icon>
(action)="edit()" <span
*ngIf="permissionsService.canEditFileAttributes(_state.file(), _state.dossier())" [innerHTML]="'file-preview.tabs.document-info.details.dossier' | translate : { dossierName: _state.dossier().dossierName }"
[tooltip]="'file-preview.tabs.document-info.edit' | translate" ></span>
buttonId="edit-document-info-btn" </div>
icon="iqser:edit"
tooltipPosition="before"
></iqser-circle-button>
<iqser-circle-button <div>
(action)="documentInfoService.hide()" <mat-icon svgIcon="iqser:document"></mat-icon>
[tooltip]="'file-preview.tabs.document-info.close' | translate" <span>{{ 'file-preview.tabs.document-info.details.pages' | translate : { pages: _state.file().numberOfPages } }}</span>
buttonId="close-document-info-btn" </div>
icon="iqser:close"
tooltipPosition="before" <div *ngIf="_state.file().added | date : 'mediumDate' as added">
></iqser-circle-button> <mat-icon svgIcon="red:calendar"></mat-icon>
<span [innerHTML]="'file-preview.tabs.document-info.details.created-on' | translate : { date: added }"></span>
</div>
<div *ngIf="_state.dossier().dueDate | date : 'mediumDate' as dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
<span [innerHTML]="'file-preview.tabs.document-info.details.due' | translate : { date: dueDate }"></span>
</div>
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ _dossierTemplate().name }}
</div> </div>
</div> </div>
</div>
<div class="right-content" iqserHasScrollbar>
<div class="section">
<div *ngFor="let attr of ctx.fileAttributes" class="attribute">
<div class="small-label">{{ attr.label }}:</div>
<div>{{ attr.value ? (isDate(attr) ? (attr.value | date : 'd MMM yyyy') : attr.value) : '-' }}</div>
</div>
</div>
<div class="section small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:folder"></mat-icon>
<span
[innerHTML]="
'file-preview.tabs.document-info.details.dossier' | translate : { dossierName: _state.dossier().dossierName }
"
></span>
</div>
<div>
<mat-icon svgIcon="iqser:document"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.pages' | translate : { pages: _state.file().numberOfPages } }}</span>
</div>
<div *ngIf="_state.file().added | date : 'mediumDate' as added">
<mat-icon svgIcon="red:calendar"></mat-icon>
<span [innerHTML]="'file-preview.tabs.document-info.details.created-on' | translate : { date: added }"></span>
</div>
<div *ngIf="_state.dossier().dueDate | date : 'mediumDate' as dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
<span [innerHTML]="'file-preview.tabs.document-info.details.due' | translate : { date: dueDate }"></span>
</div>
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ ctx.dossierTemplateName }}
</div>
</div>
</div>
</ng-container>

View File

@ -1,14 +1,13 @@
import { Component, inject } from '@angular/core'; import { Component, computed, inject } from '@angular/core';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service'; import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DocumentInfoService } from '../../services/document-info.service'; import { DocumentInfoService } from '../../services/document-info.service';
import { combineLatest, switchMap } from 'rxjs';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service'; import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { map } from 'rxjs/operators';
import { type FileAttributeConfigType, FileAttributeConfigTypes } from '@red/domain'; import { type FileAttributeConfigType, FileAttributeConfigTypes } from '@red/domain';
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service'; import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
import { FileAttributesService } from '@services/entity-services/file-attributes.service'; import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { ContextComponent } from '@iqser/common-ui'; import { ContextComponent } from '@iqser/common-ui';
import { toSignal } from '@angular/core/rxjs-interop';
interface FileAttribute { interface FileAttribute {
label: string; label: string;
@ -17,7 +16,6 @@ interface FileAttribute {
} }
interface Context { interface Context {
readonly dossierTemplateName: string;
readonly fileAttributes: FileAttribute[]; readonly fileAttributes: FileAttribute[];
} }
@ -28,28 +26,22 @@ interface Context {
}) })
export class DocumentInfoComponent extends ContextComponent<Context> { export class DocumentInfoComponent extends ContextComponent<Context> {
protected readonly _state = inject(FilePreviewStateService); protected readonly _state = inject(FilePreviewStateService);
protected readonly _dossierTemplate = toSignal(inject(DossierTemplatesService).getEntityChanged$(this._state.dossierTemplateId));
protected readonly _fileAttributesConfig = toSignal(inject(FileAttributesService).fileAttributesConfig$);
protected readonly _attributes = toSignal(
this.documentInfoService.fileAttributes$(this._state.fileId, this._state.dossierId, this._state.dossierTemplateId),
);
protected readonly _fileAttributes = computed(() => {
this._fileAttributesConfig();
return this._attributes();
});
constructor( constructor(
fileAttributesService: FileAttributesService,
readonly permissionsService: PermissionsService, readonly permissionsService: PermissionsService,
readonly documentInfoService: DocumentInfoService, readonly documentInfoService: DocumentInfoService,
private readonly _dialogService: FilePreviewDialogService, private readonly _dialogService: FilePreviewDialogService,
private readonly _dossierTemplatesService: DossierTemplatesService,
) { ) {
super(); super();
const fileAttributes$ = combineLatest([this._state.file$, this._state.dossier$, fileAttributesService.fileAttributesConfig$]).pipe(
switchMap(([file, dossier]) => this.documentInfoService.fileAttributes$(file.id, dossier.id, dossier.dossierTemplateId)),
);
const dossierTemplateName$ = this._state.dossier$.pipe(
switchMap(dossier => this._dossierTemplatesService.getEntityChanged$(dossier.dossierTemplateId)),
map(dossierTemplate => dossierTemplate.name),
);
super._initContext({
dossierTemplateName: dossierTemplateName$,
fileAttributes: fileAttributes$,
});
} }
edit() { edit() {

View File

@ -26,6 +26,7 @@
></div> ></div>
<iqser-popup-filter <iqser-popup-filter
*ngIf="documentInfoService.hidden()"
[actionsTemplate]="annotationFilterActionTemplate" [actionsTemplate]="annotationFilterActionTemplate"
[attr.help-mode-key]="'workload_in_editor'" [attr.help-mode-key]="'workload_in_editor'"
[primaryFiltersSlug]="'primaryFilters'" [primaryFiltersSlug]="'primaryFilters'"

View File

@ -75,7 +75,6 @@ import { ConfigService } from '@services/config.service';
import { ReadableRedactionsService } from '../pdf-viewer/services/readable-redactions.service'; import { ReadableRedactionsService } from '../pdf-viewer/services/readable-redactions.service';
import { Roles } from '@users/roles'; import { Roles } from '@users/roles';
import { SuggestionsService } from './services/suggestions.service'; import { SuggestionsService } from './services/suggestions.service';
import { toObservable } from '@angular/core/rxjs-interop';
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]; const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
@ -147,32 +146,41 @@ export class FilePreviewScreenComponent
const file = this.state.file(); const file = this.state.file();
this._fileDataService.loadAnnotations(file).then(); this._fileDataService.loadAnnotations(file).then();
}); });
effect(
() => {
if (this._documentViewer.loaded()) {
this._pageRotationService.clearRotations();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
this.viewerReady().then();
}
},
{ allowSignalWrites: true },
);
effect(() => {
if (this._documentViewer.pageComplete()) {
this.#setExcludedPageStyles();
}
});
effect(() => {
const selectedText = this._documentViewer.selectedText();
const canPerformActions = this.pdfProxyService.canPerformActions();
const isCurrentPageExcluded = this.state.file().isPageExcluded(this.pdf.currentPage());
if ((selectedText.length > 2 || this._isJapaneseString(selectedText)) && canPerformActions && !isCurrentPageExcluded) {
this.pdf.enable(textActions);
} else {
this.pdf.disable(textActions);
}
});
} }
get changed() { get changed() {
return this._pageRotationService.hasRotations(); return this._pageRotationService.hasRotations();
} }
get #textSelected$() {
const textSelected$ = combineLatest([
this._documentViewer.textSelected$,
toObservable(this.pdfProxyService.canPerformActions, { injector: this._injector }),
this.state.file$,
]);
return textSelected$.pipe(
tap(([selectedText, canPerformActions, file]) => {
const isCurrentPageExcluded = file.isPageExcluded(this.pdf.currentPage());
if ((selectedText.length > 2 || this._isJapaneseString(selectedText)) && canPerformActions && !isCurrentPageExcluded) {
this.pdf.enable(textActions);
} else {
this.pdf.disable(textActions);
}
}),
);
}
get #earmarks$() { get #earmarks$() {
const isEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(filter(() => this._viewModeService.isEarmarks())); const isEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(filter(() => this._viewModeService.isEarmarks()));
@ -440,17 +448,6 @@ export class FilePreviewScreenComponent
} }
loadAnnotations() { loadAnnotations() {
const documentLoaded$ = this._documentViewer.loaded$.pipe(
tap(loaded => {
if (!loaded) {
return;
}
this._pageRotationService.clearRotations();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
return this.viewerReady();
}),
);
const annotations$ = this._fileDataService.annotations$.pipe( const annotations$ = this._fileDataService.annotations$.pipe(
startWith([] as AnnotationWrapper[]), startWith([] as AnnotationWrapper[]),
pairwise(), pairwise(),
@ -471,7 +468,7 @@ export class FilePreviewScreenComponent
), ),
); );
return combineLatest([currentPageAnnotations$, documentLoaded$]).pipe( return combineLatest([currentPageAnnotations$, this._documentViewer.loaded$]).pipe(
filter(([, loaded]) => loaded), filter(([, loaded]) => loaded),
map(([annotations]) => annotations), map(([annotations]) => annotations),
map(annotations => this.drawChangedAnnotations(...annotations)), map(annotations => this.drawChangedAnnotations(...annotations)),
@ -608,23 +605,10 @@ export class FilePreviewScreenComponent
.pipe(tap(() => this.#handleDeletedFile())) .pipe(tap(() => this.#handleDeletedFile()))
.subscribe(); .subscribe();
this.addActiveScreenSubscription = combineLatest([this._viewModeService.viewMode$, this.state.file$])
.pipe(
filter(([viewMode, file]) => viewMode === ViewModes.TEXT_HIGHLIGHTS && !file.hasHighlights),
tap(() => this._viewModeService.switchToStandard()),
)
.subscribe();
this.addActiveScreenSubscription = this._documentViewer.pageComplete$.subscribe(() => {
this.#setExcludedPageStyles();
});
this.addActiveScreenSubscription = this._documentViewer.keyUp$.subscribe($event => { this.addActiveScreenSubscription = this._documentViewer.keyUp$.subscribe($event => {
this.handleKeyEvent($event); this.handleKeyEvent($event);
}); });
this.addActiveScreenSubscription = this.#textSelected$.subscribe();
this.addActiveScreenSubscription = this.#earmarks$.subscribe(); this.addActiveScreenSubscription = this.#earmarks$.subscribe();
this.addActiveScreenSubscription = this.deleteEarmarksOnViewChange$().subscribe(); this.addActiveScreenSubscription = this.deleteEarmarksOnViewChange$().subscribe();

View File

@ -23,12 +23,15 @@ export class DocumentInfoService {
) { ) {
this.hidden = computed(() => !this.#show$()); this.hidden = computed(() => !this.#show$());
this.shown = this.#show$.asReadonly(); this.shown = this.#show$.asReadonly();
effect(() => { effect(
if (this.shown()) { () => {
this._multiSelectService.deactivate(); if (this.shown()) {
this._excludedPagesService.hide(); this._multiSelectService.deactivate();
} this._excludedPagesService.hide();
}); }
},
{ allowSignalWrites: true },
);
} }
fileAttributes$(fileId: string, dossierId: string, dossierTemplateId: string) { fileAttributes$(fileId: string, dossierId: string, dossierTemplateId: string) {

View File

@ -15,6 +15,7 @@ import { TranslateService } from '@ngx-translate/core';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service'; import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { DossierDictionariesMapService } from '@services/entity-services/dossier-dictionaries-map.service'; import { DossierDictionariesMapService } from '@services/entity-services/dossier-dictionaries-map.service';
import { toSignal } from '@angular/core/rxjs-interop'; import { toSignal } from '@angular/core/rxjs-interop';
import { ViewModeService } from './view-mode.service';
const ONE_MEGABYTE = 1024 * 1024; const ONE_MEGABYTE = 1024 * 1024;
@ -64,6 +65,7 @@ export class FilePreviewStateService {
private readonly _dictionariesMapService: DictionariesMapService, private readonly _dictionariesMapService: DictionariesMapService,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _viewModeService: ViewModeService,
) { ) {
const dossiersService = dossiersServiceResolver(_injector, router); const dossiersService = dossiersServiceResolver(_injector, router);
this.dossier$ = dossiersService.getEntityChanged$(this.dossierId); this.dossier$ = dossiersService.getEntityChanged$(this.dossierId);
@ -82,12 +84,20 @@ export class FilePreviewStateService {
this.blob$ = this.#blob$; this.blob$ = this.#blob$;
this.dossierDictionary = toSignal(this._dossierDictionariesMapService.watch$(this.dossierId, 'dossier_redaction')); this.dossierDictionary = toSignal(this._dossierDictionariesMapService.watch$(this.dossierId, 'dossier_redaction'));
this.#dossierFileChange = toSignal(this.#dossierFilesChange$()); this.#dossierFileChange = toSignal(this.#dossierFilesChange$);
effect(() => { effect(() => {
if (this.#dossierFileChange()) { if (this.#dossierFileChange()) {
this._filesService.loadAll(this.dossierId); this._filesService.loadAll(this.dossierId);
} }
}); });
effect(
() => {
if (this._viewModeService.isEarmarks() && !this.file().hasHighlights) {
this._viewModeService.switchToStandard();
}
},
{ allowSignalWrites: true },
);
} }
get dictionaries(): Dictionary[] { get dictionaries(): Dictionary[] {
@ -113,6 +123,13 @@ export class FilePreviewStateService {
); );
} }
get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId),
map(() => true),
);
}
reloadBlob(): void { reloadBlob(): void {
this.#reloadBlob$.next(true); this.#reloadBlob$.next(true);
} }
@ -127,13 +144,6 @@ export class FilePreviewStateService {
return `${remainingTime} ${seconds}`; return `${remainingTime} ${seconds}`;
} }
#dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId),
map(() => true),
);
}
#downloadOriginalFile(cacheIdentifier: string, wipeCaches = true): Observable<Blob> { #downloadOriginalFile(cacheIdentifier: string, wipeCaches = true): Observable<Blob> {
const downloadFile$ = this.#getFileToDownload(cacheIdentifier); const downloadFile$ = this.#getFileToDownload(cacheIdentifier);
const obs = wipeCaches ? from(wipeCache('files')) : of({}); const obs = wipeCaches ? from(wipeCache('files')) : of({});

View File

@ -184,7 +184,7 @@ export class PdfProxyService {
private _addManualRedactionOfType(type: ManualRedactionEntryType) { private _addManualRedactionOfType(type: ManualRedactionEntryType) {
const selectedQuads: Record<string, Quad[]> = this._pdf.documentViewer.getSelectedTextQuads(); const selectedQuads: Record<string, Quad[]> = this._pdf.documentViewer.getSelectedTextQuads();
const text = this._documentViewer.selectedText; const text = this._documentViewer.selectedText();
const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true); const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested$.next({ manualRedactionEntry, type }); this.manualAnnotationRequested$.next({ manualRedactionEntry, type });
} }
@ -208,7 +208,7 @@ export class PdfProxyService {
this._pdf.enable(TEXT_POPUPS_TO_TOGGLE); this._pdf.enable(TEXT_POPUPS_TO_TOGGLE);
this._viewerHeaderService.enable(HEADER_ITEMS_TO_TOGGLE); this._viewerHeaderService.enable(HEADER_ITEMS_TO_TOGGLE);
if (this._documentViewer.selectedText.length > 2) { if (this._documentViewer.selectedText().length > 2) {
this._pdf.enable([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]); this._pdf.enable([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]);
} }
} else { } else {

View File

@ -1,4 +1,4 @@
<div [style.visibility]="(documentViewer.loaded$ | async) ? 'visible' : 'hidden'" class="pagination noselect"> <div [style.visibility]="documentViewer.loaded() ? 'visible' : 'hidden'" class="pagination noselect">
<div (click)="pdf.navigatePreviousPage()"> <div (click)="pdf.navigatePreviousPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon> <mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon>
</div> </div>
@ -8,7 +8,7 @@
#pageInput #pageInput
(change)="pdf.navigateTo(pageInput.value)" (change)="pdf.navigateTo(pageInput.value)"
[max]="pdf.totalPages()" [max]="pdf.totalPages()"
[value]="pdf.currentPage$ | async" [value]="pdf.currentPage()"
class="page-number-input" class="page-number-input"
id="currentPageNumber" id="currentPageNumber"
min="1" min="1"

View File

@ -66,8 +66,7 @@ export class AnnotationDrawService {
const annotations = annotationWrappers const annotations = annotationWrappers
?.map(annotation => this._computeAnnotation(annotation, hideSkipped, totalPages, dossierTemplateId)) ?.map(annotation => this._computeAnnotation(annotation, hideSkipped, totalPages, dossierTemplateId))
.filterTruthy(); .filterTruthy();
const documentLoaded = await firstValueFrom(this._documentViewer.loaded$); if (!this._documentViewer.loaded()) {
if (!documentLoaded) {
return; return;
} }
await this._annotationManager.add(annotations); await this._annotationManager.add(annotations);

View File

@ -1,32 +1,45 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, NgZone, Signal, signal } from '@angular/core';
import { Core } from '@pdftron/webviewer'; import { Core } from '@pdftron/webviewer';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
import { fromEvent, merge, Observable } from 'rxjs'; import { fromEvent, Observable } from 'rxjs';
import { debounceTime, filter, map, tap } from 'rxjs/operators'; import { filter, tap } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { PdfViewer } from './pdf-viewer.service'; import { PdfViewer } from './pdf-viewer.service';
import { UserPreferenceService } from '@users/user-preference.service'; import { UserPreferenceService } from '@users/user-preference.service';
import { log, shareDistinctLast, shareLast } from '@iqser/common-ui'; import { log } from '@iqser/common-ui';
import { stopAndPrevent, stopAndPreventIfNotAllowed } from '../utils/functions'; import { stopAndPrevent, stopAndPreventIfNotAllowed } from '../utils/functions';
import { RotationType, RotationTypes } from '@red/domain'; import { RotationType, RotationTypes } from '@red/domain';
import { AnnotationToolNames } from '../utils/constants'; import { AnnotationToolNames } from '../utils/constants';
import { toObservable } from '@angular/core/rxjs-interop';
import DocumentViewer = Core.DocumentViewer; import DocumentViewer = Core.DocumentViewer;
import Color = Core.Annotations.Color; import Color = Core.Annotations.Color;
import Quad = Core.Math.Quad; import Quad = Core.Math.Quad;
@Injectable() @Injectable()
export class REDDocumentViewer { export class REDDocumentViewer {
loaded$: Observable<boolean>; readonly loaded$: Observable<boolean>;
pageComplete$: Observable<boolean>;
keyUp$: Observable<KeyboardEvent>; keyUp$: Observable<KeyboardEvent>;
textSelected$: Observable<string>; readonly selectedText: Signal<string>;
selectedText = ''; readonly loaded: Signal<boolean>;
readonly pageComplete: Signal<unknown | undefined>;
#document: DocumentViewer; #document: DocumentViewer;
readonly #loaded = signal(false);
readonly #pageComplete = signal<unknown | undefined>(undefined);
readonly #selectedText = signal('');
readonly #logger = inject(NGXLogger); readonly #logger = inject(NGXLogger);
readonly #userPreferenceService = inject(UserPreferenceService); readonly #userPreferenceService = inject(UserPreferenceService);
readonly #pdf = inject(PdfViewer); readonly #pdf = inject(PdfViewer);
readonly #activatedRoute = inject(ActivatedRoute); readonly #activatedRoute = inject(ActivatedRoute);
readonly #ngZone = inject(NgZone);
constructor() {
this.loaded = this.#loaded.asReadonly();
this.loaded$ = toObservable(this.#loaded);
this.pageComplete = this.#pageComplete.asReadonly();
this.selectedText = this.#selectedText.asReadonly();
}
get PDFDoc() { get PDFDoc() {
return this.document?.getPDFDoc(); return this.document?.getPDFDoc();
@ -40,30 +53,6 @@ export class REDDocumentViewer {
return this.document?.getPageRotation(this.#document.getCurrentPage()); return this.document?.getPageRotation(this.#document.getCurrentPage());
} }
get #documentUnloaded$() {
const event$ = fromEvent(this.#document, 'documentUnloaded');
const toBool$ = event$.pipe(map(() => false));
return toBool$.pipe(tap(() => this.#logger.info('[PDF] Document unloaded')));
}
get #documentLoaded$() {
const event$ = fromEvent(this.#document, 'documentLoaded');
const toBool$ = event$.pipe(map(() => true));
return toBool$.pipe(
tap(() =>
this.#pdf.runWithCleanup(async () => {
await this.#flattenAnnotations();
this.#setCurrentPage();
this.#setInitialDisplayMode();
this.updateTooltipsVisibility();
}),
),
tap(() => this.#logger.info('[PDF] Document loaded')),
);
}
get #keyUp$() { get #keyUp$() {
return fromEvent<KeyboardEvent>(this.#document, 'keyUp').pipe( return fromEvent<KeyboardEvent>(this.#document, 'keyUp').pipe(
tap(stopAndPreventIfNotAllowed), tap(stopAndPreventIfNotAllowed),
@ -81,22 +70,6 @@ export class REDDocumentViewer {
); );
} }
get #pageComplete$() {
return fromEvent(this.#document, 'pageComplete').pipe(debounceTime(300));
}
get #textSelected$(): Observable<string> {
return fromEvent<[Quad, string, number]>(this.#document, 'textSelected').pipe(
tap(([, selectedText]) => (this.selectedText = selectedText)),
tap(([, , pageNumber]) => this.#disableTextPopupIfCompareMode(pageNumber)),
map(([, selectedText]) => selectedText),
);
}
get #loaded$() {
return merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareDistinctLast());
}
clearSelection() { clearSelection() {
this.#document.clearSelection(); this.#document.clearSelection();
this.#pdf.disable('textPopup'); this.#pdf.disable('textPopup');
@ -120,10 +93,8 @@ export class REDDocumentViewer {
init(document: DocumentViewer) { init(document: DocumentViewer) {
this.#document = document; this.#document = document;
this.loaded$ = this.#loaded$; this.#listenForDocEvents();
this.pageComplete$ = this.#pageComplete$.pipe(shareLast());
this.keyUp$ = this.#keyUp$; this.keyUp$ = this.#keyUp$;
this.textSelected$ = this.#textSelected$;
} }
async blob() { async blob() {
@ -172,6 +143,45 @@ export class REDDocumentViewer {
} }
} }
#listenForDocEvents() {
this.#document.addEventListener('textSelected', ([, selectedText, pageNumber]: [Quad, string, number]) => {
this.#ngZone.run(() => {
this.#disableTextPopupIfCompareMode(pageNumber);
this.#selectedText.set(selectedText);
});
});
this.#document.addEventListener('pageComplete', event => {
this.#ngZone.run(() => {
setTimeout(() => {
this.#pageComplete.set(event);
}, 300);
});
});
this.#document.addEventListener('documentUnloaded', () => {
this.#ngZone.run(() => {
this.#logger.info('[PDF] Document unloaded');
this.#loaded.set(false);
});
});
this.#document.addEventListener('documentLoaded', () => {
this.#ngZone.run(() => {
this.#logger.info('[PDF] Document loaded');
this.#pdf.runWithCleanup(() => {
this.#flattenAnnotations().then();
this.#setCurrentPage();
this.#setInitialDisplayMode();
this.updateTooltipsVisibility();
});
this.#loaded.set(true);
});
});
}
async #flattenAnnotations() { async #flattenAnnotations() {
const pdfDoc = await this.PDFDoc; const pdfDoc = await this.PDFDoc;
await pdfDoc.flattenAnnotations(false); await pdfDoc.flattenAnnotations(false);

View File

@ -162,7 +162,7 @@ export class FileActionsComponent implements OnChanges {
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._documentInfoService.toggle(), action: () => this._documentInfoService.toggle(),
tooltip: _('file-preview.document-info'), tooltip: _('file-preview.document-info'),
ariaExpanded: toObservable(this._documentInfoService.shown, { injector: this._injector }), ariaExpanded: toObservable(this._documentInfoService?.shown, { injector: this._injector }),
icon: 'red:status-info', icon: 'red:status-info',
show: !!this._documentInfoService, show: !!this._documentInfoService,
}, },
@ -171,7 +171,7 @@ export class FileActionsComponent implements OnChanges {
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._excludedPagesService.toggle(), action: () => this._excludedPagesService.toggle(),
tooltip: _('file-preview.exclude-pages'), tooltip: _('file-preview.exclude-pages'),
ariaExpanded: toObservable(this._excludedPagesService.shown, { injector: this._injector }), ariaExpanded: toObservable(this._excludedPagesService?.shown, { injector: this._injector }),
showDot: !!this.file.excludedPages?.length, showDot: !!this.file.excludedPages?.length,
icon: 'red:exclude-pages', icon: 'red:exclude-pages',
show: show:

View File

@ -44,7 +44,7 @@
<iqser-circle-button <iqser-circle-button
(menuClosed)="expanded = false" (menuClosed)="expanded = false"
(menuOpened)="expanded = true" (menuOpened)="expanded = true"
*ngIf="hiddenButtons.length > 0" *ngIf="hiddenButtons?.length > 0"
[attr.aria-expanded]="expanded" [attr.aria-expanded]="expanded"
[icon]="'iqser:more-actions'" [icon]="'iqser:more-actions'"
[matMenuTriggerFor]="hiddenButtonsMenu" [matMenuTriggerFor]="hiddenButtonsMenu"

View File

@ -19,9 +19,9 @@ import { MatDialog } from '@angular/material/dialog';
styleUrls: ['./expandable-file-actions.component.scss'], styleUrls: ['./expandable-file-actions.component.scss'],
}) })
export class ExpandableFileActionsComponent implements OnChanges { export class ExpandableFileActionsComponent implements OnChanges {
@Input({ required: true }) actions: Action[];
@Input() maxWidth: number; @Input() maxWidth: number;
@Input() minWidth: number; @Input() minWidth: number;
@Input() actions: Action[];
@Input() buttonType: CircleButtonType; @Input() buttonType: CircleButtonType;
@Input() tooltipPosition: IqserTooltipPosition; @Input() tooltipPosition: IqserTooltipPosition;