RED-3988: extract some events

This commit is contained in:
Dan Percic 2022-05-20 19:18:15 +03:00
parent 428975cd4e
commit 5c3c2b222c
7 changed files with 158 additions and 122 deletions

View File

@ -24,8 +24,7 @@ import { environment } from '@environments/environment';
import { AnnotationDrawService } from '../../services/annotation-draw.service'; import { AnnotationDrawService } from '../../services/annotation-draw.service';
import { AnnotationActionsService } from '../../services/annotation-actions.service'; import { AnnotationActionsService } from '../../services/annotation-actions.service';
import { UserPreferenceService } from '@services/user-preference.service'; import { UserPreferenceService } from '@services/user-preference.service';
import { BASE_HREF } from '../../../../tokens'; import { BASE_HREF_FN, BaseHrefFn } from '../../../../tokens';
import { ConfigService } from '@services/config.service';
import { AutoUnsubscribe, ConfirmationDialogInput, ErrorService, LoadingService, log } from '@iqser/common-ui'; import { AutoUnsubscribe, ConfirmationDialogInput, ErrorService, LoadingService, log } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { toPosition } from '../../utils/pdf-calculation.utils'; import { toPosition } from '../../utils/pdf-calculation.utils';
@ -33,7 +32,7 @@ import { MultiSelectService } from '../../services/multi-select.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service'; import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { PageRotationService } from '../../services/page-rotation.service'; import { PageRotationService } from '../../services/page-rotation.service';
import { ALLOWED_KEYBOARD_SHORTCUTS, HeaderElements, TextPopups } from '../../utils/constants'; import { HeaderElements, TextPopups } from '../../utils/constants';
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service'; import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
import { loadCompareDocumentWrapper } from '../../utils/compare-mode.utils'; import { loadCompareDocumentWrapper } from '../../utils/compare-mode.utils';
import { from } from 'rxjs'; import { from } from 'rxjs';
@ -55,16 +54,20 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
@Input() canPerformActions = false; @Input() canPerformActions = false;
@Output() readonly annotationSelected = this.#annotationSelected$; @Output() readonly annotationSelected = this.#annotationSelected$;
@Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>(); @Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>();
@Output() readonly pageChanged = new EventEmitter<number>(); @Output() readonly pageChanged = this.pdf.pageChanged$.pipe(tap(() => this._handleCustomActions()));
@Output() readonly keyUp = new EventEmitter<KeyboardEvent>(); @Output() readonly keyUp = this.pdf.keyUp$;
@ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef; @ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef;
instance: WebViewerInstance; instance: WebViewerInstance;
documentViewer: Core.DocumentViewer;
annotationManager: Core.AnnotationManager;
private _selectedText = ''; private _selectedText = '';
readonly #visibilityOffIcon = this._convertPath('/assets/icons/general/visibility-off.svg');
readonly #visibilityIcon = this._convertPath('/assets/icons/general/visibility.svg');
readonly #searchIcon = this._convertPath('/assets/icons/general/pdftron-action-search.svg');
readonly #falsePositiveIcon = this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg');
readonly #addRedactionIcon = this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg');
readonly #addDictIcon = this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg');
constructor( constructor(
@Inject(BASE_HREF) private readonly _baseHref: string, @Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
private readonly _manualRedactionService: ManualRedactionService, private readonly _manualRedactionService: ManualRedactionService,
private readonly _dialogService: FilePreviewDialogService, private readonly _dialogService: FilePreviewDialogService,
@ -72,7 +75,6 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
private readonly _userPreferenceService: UserPreferenceService, private readonly _userPreferenceService: UserPreferenceService,
private readonly _annotationDrawService: AnnotationDrawService, private readonly _annotationDrawService: AnnotationDrawService,
private readonly _annotationActionsService: AnnotationActionsService, private readonly _annotationActionsService: AnnotationActionsService,
private readonly _configService: ConfigService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _pageRotationService: PageRotationService, private readonly _pageRotationService: PageRotationService,
private readonly _fileDataService: FileDataService, private readonly _fileDataService: FileDataService,
@ -80,8 +82,8 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
private readonly _errorService: ErrorService, private readonly _errorService: ErrorService,
private readonly _annotationManager: REDAnnotationManager, private readonly _annotationManager: REDAnnotationManager,
readonly pdf: PdfViewer, readonly pdf: PdfViewer,
readonly stateService: FilePreviewStateService, private readonly _state: FilePreviewStateService,
readonly multiSelectService: MultiSelectService, private readonly _multiSelectService: MultiSelectService,
) { ) {
super(); super();
} }
@ -93,11 +95,11 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
ngOnInit() { ngOnInit() {
this._loadViewer(); this._loadViewer();
this.addActiveScreenSubscription = this.stateService.blob$ this.addActiveScreenSubscription = this._state.blob$
.pipe( .pipe(
log('Reload blob'), log('Reload blob'),
switchMap(blob => from(this.pdf.lockDocument()).pipe(map(() => blob))), switchMap(blob => from(this.pdf.lockDocument()).pipe(map(() => blob))),
withLatestFrom(this.stateService.file$), withLatestFrom(this._state.file$),
tap(() => this._errorService.clear()), tap(() => this._errorService.clear()),
tap(([blob, file]) => this.pdf.loadDocument(blob, file)), tap(([blob, file]) => this.pdf.loadDocument(blob, file)),
tap(() => this._pageRotationService.clearRotationsHideActions()), tap(() => this._pageRotationService.clearRotationsHideActions()),
@ -124,16 +126,16 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
fileReader.onload = async () => { fileReader.onload = async () => {
const pdfNet = this.instance.Core.PDFNet; const pdfNet = this.instance.Core.PDFNet;
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null); await pdfNet.initialize(environment.licenseKey ? window.atob(environment.licenseKey) : null);
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer); const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer);
const blob = await this.stateService.blob; const blob = await this._state.blob;
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer()); const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
const loadCompareDocument = async () => { const loadCompareDocument = async () => {
this._loadingService.start(); this._loadingService.start();
const mergedDocument = await pdfNet.PDFDoc.create(); const mergedDocument = await pdfNet.PDFDoc.create();
const file = this.stateService.file; const file = this._state.file;
await loadCompareDocumentWrapper( await loadCompareDocumentWrapper(
currentDocument, currentDocument,
compareDocument, compareDocument,
@ -184,13 +186,13 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
if (action === 'deselected') { if (action === 'deselected') {
// Remove deselected annotations from selected list // Remove deselected annotations from selected list
nextAnnotations = this.annotationManager.getSelectedAnnotations().filter(ann => !annotations.some(a => a.Id === ann.Id)); nextAnnotations = this._annotationManager.selectedAnnotations.filter(ann => !annotations.some(a => a.Id === ann.Id));
} else if (!this.multiSelectService.isEnabled) { } else if (!this._multiSelectService.isEnabled) {
// Only choose the last selected annotation, to bypass viewer multi select // Only choose the last selected annotation, to bypass viewer multi select
nextAnnotations = annotations; nextAnnotations = annotations;
} else { } else {
// Get selected annotations from the manager, no intervention needed // Get selected annotations from the manager, no intervention needed
nextAnnotations = this.annotationManager.getSelectedAnnotations(); nextAnnotations = this._annotationManager.selectedAnnotations;
} }
// this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id)); // this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id));
@ -199,7 +201,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
return nextAnnotations.map(ann => ann.Id); return nextAnnotations.map(ann => ann.Id);
} }
if (!this.multiSelectService.isEnabled) { if (!this._multiSelectService.isEnabled) {
const notSelected = this._fileDataService.all.filter(wrapper => !nextAnnotations.some(ann => ann.Id === wrapper.id)); const notSelected = this._fileDataService.all.filter(wrapper => !nextAnnotations.some(ann => ann.Id === wrapper.id));
this._annotationManager.deselectAnnotations(notSelected); this._annotationManager.deselectAnnotations(notSelected);
} }
@ -211,87 +213,35 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
private _loadViewer() { private _loadViewer() {
this.instance = this.pdf.instance; this.instance = this.pdf.instance;
this.documentViewer = this.pdf.documentViewer;
this.annotationManager = this.pdf.annotationManager;
this._configureElements(); this._configureElements();
this._configureTextPopup(); this._configureTextPopup();
this.annotationManager.addEventListener('annotationChanged', (annotations: Annotation[]) => { this.pdf.documentViewer.addEventListener('textSelected', (quads, selectedText, pageNumber: number) => {
// when a rectangle is drawn,
// it returns one annotation with tool name 'AnnotationCreateRectangle;
// this will auto select rectangle after drawing
if (annotations.length === 1 && annotations[0].ToolName === 'AnnotationCreateRectangle') {
this.annotationManager.selectAnnotations(annotations);
annotations[0].disableRotationControl();
}
});
this.documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
this._annotationManager.deselectAnnotations();
console.log(pageNumber);
this._ngZone.run(() => this.pageChanged.emit(pageNumber));
return this._handleCustomActions();
});
this.documentViewer.addEventListener('keyUp', ($event: KeyboardEvent) => {
// arrows and full-screen
if (($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input') {
if ($event.key.startsWith('Arrow') || $event.key === 'f') {
this._ngZone.run(() => this.keyUp.emit($event));
$event.preventDefault();
$event.stopPropagation();
}
}
if (!ALLOWED_KEYBOARD_SHORTCUTS.includes($event.key)) {
$event.preventDefault();
$event.stopPropagation();
}
});
this.documentViewer.addEventListener('textSelected', (quads, selectedText, pageNumber: number) => {
this._selectedText = selectedText; this._selectedText = selectedText;
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
const file = this.stateService.file;
if (this.pdf.isCompare && pageNumber % 2 === 0) { if (this.pdf.isCompare && pageNumber % 2 === 0) {
this.instance.UI.disableElements(['textPopup']); this.pdf.disable('textPopup');
} else { } else {
this.instance.UI.enableElements(['textPopup']); this.pdf.enable('textPopup');
} }
const isCurrentPageExcluded = file.isPageExcluded(this.pdf.currentPage); const isCurrentPageExcluded = this._state.file.isPageExcluded(this.pdf.currentPage);
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
if (selectedText.length > 2 && this.canPerformActions && !isCurrentPageExcluded) { if (selectedText.length > 2 && this.canPerformActions && !isCurrentPageExcluded) {
this.instance.UI.enableElements(textActions); this.pdf.enable(textActions);
} else { } else {
this.instance.UI.disableElements(textActions); this.pdf.disable(textActions);
} }
}); });
this.instance.UI.iframeWindow.addEventListener('visibilityChanged', (event: any) => {
if (event.detail.element === 'searchPanel') {
const inputElement = this.instance.UI.iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement;
setTimeout(() => {
inputElement.value = '';
}, 0);
if (!event.detail.isVisible) {
this.documentViewer.clearSearchResults();
}
}
});
}
private _convertPath(path: string): string {
return this._baseHref + path;
} }
private _toggleRectangleAnnotationAction(readonly = false) { private _toggleRectangleAnnotationAction(readonly = false) {
if (!readonly) { if (!readonly) {
this.instance.UI.enableElements([TextPopups.ADD_RECTANGLE]); this.pdf.enable(TextPopups.ADD_RECTANGLE);
} else { } else {
this.instance.UI.disableElements([TextPopups.ADD_RECTANGLE]); this.pdf.disable(TextPopups.ADD_RECTANGLE);
} }
} }
@ -299,13 +249,8 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
this._headerConfigService.initialize(this.compareFileInput); this._headerConfigService.initialize(this.compareFileInput);
const dossierTemplateId = this.dossier.dossierTemplateId; const dossierTemplateId = this.dossier.dossierTemplateId;
const color = this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual');
this.documentViewer.getTool('AnnotationCreateRectangle').setStyles({ this.pdf.setRectangleToolStyles(color);
StrokeThickness: 2,
StrokeColor: this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual'),
FillColor: this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual'),
Opacity: 0.6,
});
} }
#configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) { #configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) {
@ -332,18 +277,16 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
this.instance.UI.annotationPopup.add([ this.instance.UI.annotationPopup.add([
{ {
type: 'actionButton', type: 'actionButton',
img: allAreVisible img: allAreVisible ? this.#visibilityOffIcon : this.#visibilityIcon,
? this._convertPath('/assets/icons/general/visibility-off.svg')
: this._convertPath('/assets/icons/general/visibility.svg'),
title: this._translateService.instant(`annotation-actions.${allAreVisible ? 'hide' : 'show'}`), title: this._translateService.instant(`annotation-actions.${allAreVisible ? 'hide' : 'show'}`),
onClick: () => { onClick: () => {
this._ngZone.run(() => { this._ngZone.run(() => {
if (allAreVisible) { if (allAreVisible) {
this.annotationManager.hideAnnotations(viewerAnnotations); this._annotationManager.hideAnnotations(viewerAnnotations);
} else { } else {
this.annotationManager.showAnnotations(viewerAnnotations); this._annotationManager.showAnnotations(viewerAnnotations);
} }
this.annotationManager.deselectAllAnnotations(); this._annotationManager.deselectAnnotations();
this._fileDataService.updateHiddenAnnotations(viewerAnnotations, allAreVisible); this._fileDataService.updateHiddenAnnotations(viewerAnnotations, allAreVisible);
}); });
}, },
@ -361,7 +304,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
{ {
type: 'actionButton', type: 'actionButton',
dataElement: TextPopups.ADD_RECTANGLE, dataElement: TextPopups.ADD_RECTANGLE,
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), img: this.#addRedactionIcon,
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION), title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
onClick: () => this._addRectangleManualRedaction(), onClick: () => this._addRectangleManualRedaction(),
}, },
@ -370,7 +313,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
} }
private _addRectangleManualRedaction() { private _addRectangleManualRedaction() {
const activeAnnotation = this.annotationManager.getSelectedAnnotations()[0]; const activeAnnotation = this._annotationManager.selectedAnnotations[0];
const activePage = activeAnnotation.getPageNumber(); const activePage = activeAnnotation.getPageNumber();
const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation)]; const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation)];
const manualRedactionEntry = this._getManualRedaction({ [activePage]: quads }); const manualRedactionEntry = this._getManualRedaction({ [activePage]: quads });
@ -387,10 +330,10 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
private _configureTextPopup() { private _configureTextPopup() {
const searchButton = { const searchButton = {
type: 'actionButton', type: 'actionButton',
img: this._convertPath('/assets/icons/general/pdftron-action-search.svg'), img: this.#searchIcon,
title: this._translateService.instant('pdf-viewer.text-popup.actions.search'), title: this._translateService.instant('pdf-viewer.text-popup.actions.search'),
onClick: () => { onClick: () => {
const text = this.documentViewer.getSelectedText(); const text = this.pdf.documentViewer.getSelectedText();
const searchOptions = { const searchOptions = {
caseSensitive: true, // match case caseSensitive: true, // match case
wholeWord: true, // match whole words only wholeWord: true, // match whole words only
@ -410,7 +353,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
popups.push({ popups.push({
type: 'actionButton', type: 'actionButton',
dataElement: TextPopups.ADD_FALSE_POSITIVE, dataElement: TextPopups.ADD_FALSE_POSITIVE,
img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'), img: this.#falsePositiveIcon,
title: this.#getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE), title: this.#getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE), onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE),
}); });
@ -419,14 +362,14 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
popups.push({ popups.push({
type: 'actionButton', type: 'actionButton',
dataElement: TextPopups.ADD_REDACTION, dataElement: TextPopups.ADD_REDACTION,
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), img: this.#addRedactionIcon,
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION), title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION), onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION),
}); });
popups.push({ popups.push({
type: 'actionButton', type: 'actionButton',
dataElement: TextPopups.ADD_DICTIONARY, dataElement: TextPopups.ADD_DICTIONARY,
img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'), img: this.#addDictIcon,
title: this.#getTitle(ManualRedactionEntryTypes.DICTIONARY), title: this.#getTitle(ManualRedactionEntryTypes.DICTIONARY),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY), onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY),
}); });
@ -441,8 +384,8 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
} }
private _addManualRedactionOfType(type: ManualRedactionEntryType) { private _addManualRedactionOfType(type: ManualRedactionEntryType) {
const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.documentViewer.getSelectedTextQuads(); const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.pdf.documentViewer.getSelectedTextQuads();
const text = this.documentViewer.getSelectedText(); const text = this.pdf.documentViewer.getSelectedText();
const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true); const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested.emit({ manualRedactionEntry, type }); this.manualAnnotationRequested.emit({ manualRedactionEntry, type });
} }
@ -455,7 +398,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
HeaderElements.ROTATE_RIGHT_BUTTON, HeaderElements.ROTATE_RIGHT_BUTTON,
]; ];
const isCurrentPageExcluded = this.stateService.file.isPageExcluded(this.pdf.currentPage); const isCurrentPageExcluded = this._state.file.isPageExcluded(this.pdf.currentPage);
if (this.canPerformActions && !isCurrentPageExcluded) { if (this.canPerformActions && !isCurrentPageExcluded) {
try { try {
@ -463,11 +406,11 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
} catch (e) { } catch (e) {
// happens // happens
} }
this.instance.UI.enableElements(textPopupsToToggle); this.pdf.enable(textPopupsToToggle);
this._headerConfigService.enable(headerItemsToToggle); this._headerConfigService.enable(headerItemsToToggle);
if (this._selectedText.length > 2) { if (this._selectedText.length > 2) {
this.instance.UI.enableElements([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]); this.pdf.enable([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]);
} }
return; return;
@ -488,7 +431,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
this.instance.UI.disableTools(['AnnotationCreateRectangle']); this.instance.UI.disableTools(['AnnotationCreateRectangle']);
} }
this.instance.UI.disableElements(textPopupElementsToDisable); this.pdf.disable(textPopupElementsToDisable);
this._headerConfigService.disable(headerElementsToDisable); this._headerConfigService.disable(headerElementsToDisable);
} }
@ -502,7 +445,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
for (const key of Object.keys(quads)) { for (const key of Object.keys(quads)) {
for (const quad of quads[key]) { for (const quad of quads[key]) {
const page = parseInt(key, 10); const page = parseInt(key, 10);
const pageHeight = this.documentViewer.getPageHeight(page); const pageHeight = this.pdf.documentViewer.getPageHeight(page);
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.#translateQuad(page, quad) : quad)); entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.#translateQuad(page, quad) : quad));
} }
} }

View File

@ -11,8 +11,6 @@ export const ActionsHelpModeKeys = {
'hint-image': 'picture', 'hint-image': 'picture',
} as const; } as const;
export const ALLOWED_KEYBOARD_SHORTCUTS: List = ['+', '-', 'p', 'r', 'Escape'] as const;
export const ALL_HOTKEYS: List = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'] as const; export const ALL_HOTKEYS: List = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'] as const;
export type HeaderElementType = export type HeaderElementType =

View File

@ -1,19 +1,14 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Core } from '@pdftron/webviewer'; import { Core } from '@pdftron/webviewer';
import type { List } from '@iqser/common-ui'; import type { List } from '@iqser/common-ui';
import { DeleteAnnotationsOptions } from '@shared/components/reusable-pdf-viewer/types'; import { AnnotationPredicate, DeleteAnnotationsOptions } from '@shared/components/reusable-pdf-viewer/types';
import type { AnnotationWrapper } from '@models/file/annotation.wrapper'; import type { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { fromEvent, Observable } from 'rxjs'; import { fromEvent, Observable } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { getIds } from '@shared/components/reusable-pdf-viewer/functions';
import AnnotationManager = Core.AnnotationManager; import AnnotationManager = Core.AnnotationManager;
import Annotation = Core.Annotations.Annotation; import Annotation = Core.Annotations.Annotation;
type AnnotationPredicate = (value: Annotation) => boolean;
function getIds(items?: List<string | AnnotationWrapper>): List | undefined {
return items?.map(value => (typeof value === 'string' ? value : value.id));
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@ -37,6 +32,7 @@ export class REDAnnotationManager {
init(annotationManager: AnnotationManager) { init(annotationManager: AnnotationManager) {
this.#manager = annotationManager; this.#manager = annotationManager;
this.annotationSelected$ = this.#annotationSelected$; this.annotationSelected$ = this.#annotationSelected$;
this.#autoSelectRectangleAfterCreation();
} }
deleteAnnotation(annotation: AnnotationWrapper | string) { deleteAnnotation(annotation: AnnotationWrapper | string) {
@ -90,6 +86,18 @@ export class REDAnnotationManager {
this.#manager.showAnnotations(annotations); this.#manager.showAnnotations(annotations);
} }
#autoSelectRectangleAfterCreation() {
this.#manager.addEventListener('annotationChanged', (annotations: Annotation[]) => {
// when a rectangle is drawn,
// it returns one annotation with tool name 'AnnotationCreateRectangle;
// this will auto select rectangle after drawing
if (annotations.length === 1 && annotations[0].ToolName === 'AnnotationCreateRectangle') {
this.#manager.selectAnnotations(annotations);
annotations[0].disableRotationControl();
}
});
}
#getAnnotationsById(ids: List) { #getAnnotationsById(ids: List) {
return ids.map(id => this.#manager.getAnnotationById(id)).filter(a => !!a); return ids.map(id => this.#manager.getAnnotationById(id)).filter(a => !!a);
} }

View File

@ -1,6 +1,8 @@
import { CustomError } from '@iqser/common-ui'; import { CustomError, List } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export const ALLOWED_KEYBOARD_SHORTCUTS: List = ['+', '-', 'p', 'r', 'Escape'] as const;
export const DOCUMENT_LOADING_ERROR = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh'); export const DOCUMENT_LOADING_ERROR = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh');
export const USELESS_ELEMENTS = [ export const USELESS_ELEMENTS = [

View File

@ -0,0 +1,22 @@
import { List } from '@iqser/common-ui';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ALLOWED_KEYBOARD_SHORTCUTS } from '@shared/components/reusable-pdf-viewer/constants';
export function stopAndPrevent<T extends Event>($event: T) {
$event.preventDefault();
$event.stopPropagation();
}
export function stopAndPreventIfNotAllowed($event: KeyboardEvent) {
if (!ALLOWED_KEYBOARD_SHORTCUTS.includes($event.key)) {
stopAndPrevent($event);
}
}
export function getIds(items?: List<string | AnnotationWrapper>): List | undefined {
return items?.map(value => (typeof value === 'string' ? value : value.id));
}
export function asList(dataElements: string[] | string): string[] {
return typeof dataElements === 'string' ? [dataElements] : dataElements;
}

View File

@ -3,20 +3,23 @@ import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/w
import { environment } from '@environments/environment'; import { environment } from '@environments/environment';
import { BASE_HREF_FN } from '../../../../tokens'; import { BASE_HREF_FN } from '../../../../tokens';
import { File } from '@red/domain'; import { File } from '@red/domain';
import { ErrorService, shareDistinctLast, shareLast } from '@iqser/common-ui'; import { ErrorService, log, shareDistinctLast, shareLast } from '@iqser/common-ui';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, filter, map, startWith, tap } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, fromEvent, merge, Observable } from 'rxjs'; import { BehaviorSubject, combineLatest, fromEvent, merge, Observable } from 'rxjs';
import { ConfigService } from '@services/config.service'; import { ConfigService } from '@services/config.service';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, USELESS_ELEMENTS } from './constants'; import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, USELESS_ELEMENTS } from './constants';
import { Rgb } from '@shared/components/reusable-pdf-viewer/types'; import { Rgb } from '@shared/components/reusable-pdf-viewer/types';
import { UserPreferenceService } from '@services/user-preference.service'; import { UserPreferenceService } from '@services/user-preference.service';
import { asList, stopAndPrevent, stopAndPreventIfNotAllowed } from '@shared/components/reusable-pdf-viewer/functions';
import { REDAnnotationManager } from '@shared/components/reusable-pdf-viewer/annotation-manager.service';
import AnnotationManager = Core.AnnotationManager; import AnnotationManager = Core.AnnotationManager;
import TextTool = Core.Tools.TextTool; import TextTool = Core.Tools.TextTool;
import Annotation = Core.Annotations.Annotation; import Annotation = Core.Annotations.Annotation;
import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation; import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation;
import DocumentViewer = Core.DocumentViewer; import DocumentViewer = Core.DocumentViewer;
import Color = Core.Annotations.Color;
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -34,8 +37,10 @@ export class PdfViewer {
loaded$: Observable<boolean>; loaded$: Observable<boolean>;
pageComplete$: Observable<unknown>; pageComplete$: Observable<unknown>;
pageChanged$: Observable<number>;
compareMode$: Observable<boolean>; compareMode$: Observable<boolean>;
totalPages$: Observable<number>; totalPages$: Observable<number>;
keyUp$: Observable<KeyboardEvent>;
#instance: WebViewerInstance; #instance: WebViewerInstance;
readonly #compareMode$ = new BehaviorSubject(false); readonly #compareMode$ = new BehaviorSubject(false);
@ -43,6 +48,7 @@ export class PdfViewer {
constructor( constructor(
private readonly _logger: NGXLogger, private readonly _logger: NGXLogger,
private readonly _activatedRoute: ActivatedRoute, private readonly _activatedRoute: ActivatedRoute,
private readonly _annotationManager: REDAnnotationManager,
private readonly _userPreferenceService: UserPreferenceService, private readonly _userPreferenceService: UserPreferenceService,
private readonly _injector: Injector, private readonly _injector: Injector,
) {} ) {}
@ -81,8 +87,18 @@ export class PdfViewer {
return this.isCompare ? Math.ceil(currentInternalPage / 2) : currentInternalPage; return this.isCompare ? Math.ceil(currentInternalPage / 2) : currentInternalPage;
} }
get #keyUp$() {
return fromEvent<KeyboardEvent>(this.documentViewer, 'keyUp').pipe(
tap(stopAndPreventIfNotAllowed),
filter($event => ($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input'),
filter($event => $event.key.startsWith('Arrow') || $event.key === 'f'),
tap(stopAndPrevent),
log('[PDF] Keyboard shortcut'),
);
}
get #totalPages$() { get #totalPages$() {
const layoutChanged$ = fromEvent(this.documentViewer, 'layoutChanged'); const layoutChanged$ = fromEvent(this.documentViewer, 'layoutChanged').pipe(startWith(''));
const pageCount$ = layoutChanged$.pipe( const pageCount$ = layoutChanged$.pipe(
map(() => this.pageCount), map(() => this.pageCount),
distinctUntilChanged(), distinctUntilChanged(),
@ -123,6 +139,11 @@ export class PdfViewer {
return this.documentViewer.getCurrentPage(); return this.documentViewer.getCurrentPage();
} }
get #pageChanged$() {
const page$ = fromEvent<number>(this.documentViewer, 'pageNumberUpdated');
return page$.pipe(tap(() => this._annotationManager.deselectAnnotations()));
}
navigateTo(page: string | number) { navigateTo(page: string | number) {
const parsedNumber = typeof page === 'string' ? parseInt(page, 10) : page; const parsedNumber = typeof page === 'string' ? parseInt(page, 10) : page;
const paginationOffset = this.#paginationOffset; const paginationOffset = this.#paginationOffset;
@ -152,14 +173,34 @@ export class PdfViewer {
this.loaded$ = merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast()); this.loaded$ = merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast());
this.compareMode$ = this.#compareMode$.asObservable(); this.compareMode$ = this.#compareMode$.asObservable();
this.pageComplete$ = this.#pageComplete$.pipe(shareLast()); this.pageComplete$ = this.#pageComplete$.pipe(shareLast());
this.pageChanged$ = this.#pageChanged$.pipe(shareLast());
this.totalPages$ = this.#totalPages$.pipe(shareDistinctLast()); this.totalPages$ = this.#totalPages$.pipe(shareDistinctLast());
this.keyUp$ = this.#keyUp$;
this.#setSelectionMode(); this.#setSelectionMode();
this.#configureElements(); this.#configureElements();
this.#disableHotkeys(); this.#disableHotkeys();
this.#clearSearchResultsWhenVisibilityChanged();
return this.#instance; return this.#instance;
} }
setRectangleToolStyles(color: Color) {
this.documentViewer.getTool('AnnotationCreateRectangle').setStyles({
StrokeThickness: 2,
StrokeColor: color,
FillColor: color,
Opacity: 0.6,
});
}
enable(dataElements: string[] | string) {
this.#instance.UI.enableElements(asList(dataElements));
}
disable(dataElements: string[] | string) {
this.#instance.UI.disableElements(asList(dataElements));
}
getPageHeight(page: number) { getPageHeight(page: number) {
try { try {
return this.documentViewer.getPageHeight(page); return this.documentViewer.getPageHeight(page);
@ -235,6 +276,23 @@ export class PdfViewer {
return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation; return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation;
} }
#clearSearchResultsWhenVisibilityChanged() {
const iframeWindow = this.#instance.UI.iframeWindow;
iframeWindow.addEventListener('visibilityChanged', (event: any) => {
if (event.detail.element !== 'searchPanel') {
return;
}
const inputElement = iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement;
setTimeout(() => (inputElement.value = ''), 0);
if (!event.detail.isVisible) {
this.documentViewer.clearSearchResults();
}
});
}
#setInitialDisplayMode() { #setInitialDisplayMode() {
this.#instance.UI.setFitMode('FitPage'); this.#instance.UI.setFitMode('FitPage');
const displayModeManager = this.documentViewer.getDisplayModeManager(); const displayModeManager = this.documentViewer.getDisplayModeManager();

View File

@ -1,3 +1,6 @@
import { Core } from '@pdftron/webviewer';
import Annotation = Core.Annotations.Annotation;
export interface Rgb { export interface Rgb {
readonly r: number; readonly r: number;
readonly g: number; readonly g: number;
@ -10,3 +13,5 @@ export interface DeleteAnnotationsOptions {
readonly isUndoRedo?: boolean; readonly isUndoRedo?: boolean;
readonly source?: string; readonly source?: string;
} }
export type AnnotationPredicate = (value: Annotation) => boolean;