From 5c3c2b222ce31a79a2f3fcd4d3ec8e3653f47c73 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Fri, 20 May 2022 19:18:15 +0300 Subject: [PATCH] RED-3988: extract some events --- .../pdf-paginator/pdf-paginator.component.ts | 161 ++++++------------ .../modules/file-preview/utils/constants.ts | 2 - .../annotation-manager.service.ts | 22 ++- .../reusable-pdf-viewer/constants.ts | 4 +- .../reusable-pdf-viewer/functions.ts | 22 +++ .../reusable-pdf-viewer/pdf-viewer.service.ts | 64 ++++++- .../components/reusable-pdf-viewer/types.ts | 5 + 7 files changed, 158 insertions(+), 122 deletions(-) create mode 100644 apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/functions.ts diff --git a/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.ts b/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.ts index ed1b0d8b8..0b0b3e22c 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.ts @@ -24,8 +24,7 @@ import { environment } from '@environments/environment'; import { AnnotationDrawService } from '../../services/annotation-draw.service'; import { AnnotationActionsService } from '../../services/annotation-actions.service'; import { UserPreferenceService } from '@services/user-preference.service'; -import { BASE_HREF } from '../../../../tokens'; -import { ConfigService } from '@services/config.service'; +import { BASE_HREF_FN, BaseHrefFn } from '../../../../tokens'; import { AutoUnsubscribe, ConfirmationDialogInput, ErrorService, LoadingService, log } from '@iqser/common-ui'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; 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 { map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; 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 { loadCompareDocumentWrapper } from '../../utils/compare-mode.utils'; import { from } from 'rxjs'; @@ -55,16 +54,20 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On @Input() canPerformActions = false; @Output() readonly annotationSelected = this.#annotationSelected$; @Output() readonly manualAnnotationRequested = new EventEmitter(); - @Output() readonly pageChanged = new EventEmitter(); - @Output() readonly keyUp = new EventEmitter(); + @Output() readonly pageChanged = this.pdf.pageChanged$.pipe(tap(() => this._handleCustomActions())); + @Output() readonly keyUp = this.pdf.keyUp$; @ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef; instance: WebViewerInstance; - documentViewer: Core.DocumentViewer; - annotationManager: Core.AnnotationManager; 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( - @Inject(BASE_HREF) private readonly _baseHref: string, + @Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn, private readonly _translateService: TranslateService, private readonly _manualRedactionService: ManualRedactionService, private readonly _dialogService: FilePreviewDialogService, @@ -72,7 +75,6 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On private readonly _userPreferenceService: UserPreferenceService, private readonly _annotationDrawService: AnnotationDrawService, private readonly _annotationActionsService: AnnotationActionsService, - private readonly _configService: ConfigService, private readonly _loadingService: LoadingService, private readonly _pageRotationService: PageRotationService, private readonly _fileDataService: FileDataService, @@ -80,8 +82,8 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On private readonly _errorService: ErrorService, private readonly _annotationManager: REDAnnotationManager, readonly pdf: PdfViewer, - readonly stateService: FilePreviewStateService, - readonly multiSelectService: MultiSelectService, + private readonly _state: FilePreviewStateService, + private readonly _multiSelectService: MultiSelectService, ) { super(); } @@ -93,11 +95,11 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On ngOnInit() { this._loadViewer(); - this.addActiveScreenSubscription = this.stateService.blob$ + this.addActiveScreenSubscription = this._state.blob$ .pipe( log('Reload blob'), switchMap(blob => from(this.pdf.lockDocument()).pipe(map(() => blob))), - withLatestFrom(this.stateService.file$), + withLatestFrom(this._state.file$), tap(() => this._errorService.clear()), tap(([blob, file]) => this.pdf.loadDocument(blob, file)), tap(() => this._pageRotationService.clearRotationsHideActions()), @@ -124,16 +126,16 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On fileReader.onload = async () => { 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 blob = await this.stateService.blob; + const blob = await this._state.blob; const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer()); const loadCompareDocument = async () => { this._loadingService.start(); const mergedDocument = await pdfNet.PDFDoc.create(); - const file = this.stateService.file; + const file = this._state.file; await loadCompareDocumentWrapper( currentDocument, compareDocument, @@ -184,13 +186,13 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On if (action === 'deselected') { // Remove deselected annotations from selected list - nextAnnotations = this.annotationManager.getSelectedAnnotations().filter(ann => !annotations.some(a => a.Id === ann.Id)); - } else if (!this.multiSelectService.isEnabled) { + nextAnnotations = this._annotationManager.selectedAnnotations.filter(ann => !annotations.some(a => a.Id === ann.Id)); + } else if (!this._multiSelectService.isEnabled) { // Only choose the last selected annotation, to bypass viewer multi select nextAnnotations = annotations; } else { // 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)); @@ -199,7 +201,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On 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)); this._annotationManager.deselectAnnotations(notSelected); } @@ -211,87 +213,35 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On private _loadViewer() { this.instance = this.pdf.instance; - this.documentViewer = this.pdf.documentViewer; - this.annotationManager = this.pdf.annotationManager; this._configureElements(); this._configureTextPopup(); - this.annotationManager.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.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.pdf.documentViewer.addEventListener('textSelected', (quads, selectedText, pageNumber: number) => { this._selectedText = selectedText; - const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]; - - const file = this.stateService.file; if (this.pdf.isCompare && pageNumber % 2 === 0) { - this.instance.UI.disableElements(['textPopup']); + this.pdf.disable('textPopup'); } 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) { - this.instance.UI.enableElements(textActions); + this.pdf.enable(textActions); } 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) { if (!readonly) { - this.instance.UI.enableElements([TextPopups.ADD_RECTANGLE]); + this.pdf.enable(TextPopups.ADD_RECTANGLE); } 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); const dossierTemplateId = this.dossier.dossierTemplateId; - - this.documentViewer.getTool('AnnotationCreateRectangle').setStyles({ - StrokeThickness: 2, - StrokeColor: this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual'), - FillColor: this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual'), - Opacity: 0.6, - }); + const color = this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual'); + this.pdf.setRectangleToolStyles(color); } #configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) { @@ -332,18 +277,16 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On this.instance.UI.annotationPopup.add([ { type: 'actionButton', - img: allAreVisible - ? this._convertPath('/assets/icons/general/visibility-off.svg') - : this._convertPath('/assets/icons/general/visibility.svg'), + img: allAreVisible ? this.#visibilityOffIcon : this.#visibilityIcon, title: this._translateService.instant(`annotation-actions.${allAreVisible ? 'hide' : 'show'}`), onClick: () => { this._ngZone.run(() => { if (allAreVisible) { - this.annotationManager.hideAnnotations(viewerAnnotations); + this._annotationManager.hideAnnotations(viewerAnnotations); } else { - this.annotationManager.showAnnotations(viewerAnnotations); + this._annotationManager.showAnnotations(viewerAnnotations); } - this.annotationManager.deselectAllAnnotations(); + this._annotationManager.deselectAnnotations(); this._fileDataService.updateHiddenAnnotations(viewerAnnotations, allAreVisible); }); }, @@ -361,7 +304,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On { type: 'actionButton', dataElement: TextPopups.ADD_RECTANGLE, - img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), + img: this.#addRedactionIcon, title: this.#getTitle(ManualRedactionEntryTypes.REDACTION), onClick: () => this._addRectangleManualRedaction(), }, @@ -370,7 +313,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On } private _addRectangleManualRedaction() { - const activeAnnotation = this.annotationManager.getSelectedAnnotations()[0]; + const activeAnnotation = this._annotationManager.selectedAnnotations[0]; const activePage = activeAnnotation.getPageNumber(); const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation)]; const manualRedactionEntry = this._getManualRedaction({ [activePage]: quads }); @@ -387,10 +330,10 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On private _configureTextPopup() { const searchButton = { 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'), onClick: () => { - const text = this.documentViewer.getSelectedText(); + const text = this.pdf.documentViewer.getSelectedText(); const searchOptions = { caseSensitive: true, // match case wholeWord: true, // match whole words only @@ -410,7 +353,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On popups.push({ type: 'actionButton', 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), onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE), }); @@ -419,14 +362,14 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On popups.push({ type: 'actionButton', dataElement: TextPopups.ADD_REDACTION, - img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), + img: this.#addRedactionIcon, title: this.#getTitle(ManualRedactionEntryTypes.REDACTION), onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION), }); popups.push({ type: 'actionButton', dataElement: TextPopups.ADD_DICTIONARY, - img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'), + img: this.#addDictIcon, title: this.#getTitle(ManualRedactionEntryTypes.DICTIONARY), onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY), }); @@ -441,8 +384,8 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On } private _addManualRedactionOfType(type: ManualRedactionEntryType) { - const selectedQuads: Readonly> = this.documentViewer.getSelectedTextQuads(); - const text = this.documentViewer.getSelectedText(); + const selectedQuads: Readonly> = this.pdf.documentViewer.getSelectedTextQuads(); + const text = this.pdf.documentViewer.getSelectedText(); const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true); this.manualAnnotationRequested.emit({ manualRedactionEntry, type }); } @@ -455,7 +398,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On 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) { try { @@ -463,11 +406,11 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On } catch (e) { // happens } - this.instance.UI.enableElements(textPopupsToToggle); + this.pdf.enable(textPopupsToToggle); this._headerConfigService.enable(headerItemsToToggle); 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; @@ -488,7 +431,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On this.instance.UI.disableTools(['AnnotationCreateRectangle']); } - this.instance.UI.disableElements(textPopupElementsToDisable); + this.pdf.disable(textPopupElementsToDisable); 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 quad of quads[key]) { 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)); } } diff --git a/apps/red-ui/src/app/modules/file-preview/utils/constants.ts b/apps/red-ui/src/app/modules/file-preview/utils/constants.ts index 7e72ddf76..b03361b6f 100644 --- a/apps/red-ui/src/app/modules/file-preview/utils/constants.ts +++ b/apps/red-ui/src/app/modules/file-preview/utils/constants.ts @@ -11,8 +11,6 @@ export const ActionsHelpModeKeys = { 'hint-image': 'picture', } 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 type HeaderElementType = diff --git a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/annotation-manager.service.ts b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/annotation-manager.service.ts index c02f729aa..5d2d23f91 100644 --- a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/annotation-manager.service.ts +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/annotation-manager.service.ts @@ -1,19 +1,14 @@ import { Injectable } from '@angular/core'; import { Core } from '@pdftron/webviewer'; 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 { fromEvent, Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; +import { getIds } from '@shared/components/reusable-pdf-viewer/functions'; import AnnotationManager = Core.AnnotationManager; import Annotation = Core.Annotations.Annotation; -type AnnotationPredicate = (value: Annotation) => boolean; - -function getIds(items?: List): List | undefined { - return items?.map(value => (typeof value === 'string' ? value : value.id)); -} - @Injectable({ providedIn: 'root', }) @@ -37,6 +32,7 @@ export class REDAnnotationManager { init(annotationManager: AnnotationManager) { this.#manager = annotationManager; this.annotationSelected$ = this.#annotationSelected$; + this.#autoSelectRectangleAfterCreation(); } deleteAnnotation(annotation: AnnotationWrapper | string) { @@ -90,6 +86,18 @@ export class REDAnnotationManager { 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) { return ids.map(id => this.#manager.getAnnotationById(id)).filter(a => !!a); } diff --git a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/constants.ts b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/constants.ts index b51df7e60..f9282494c 100644 --- a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/constants.ts +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/constants.ts @@ -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'; +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 USELESS_ELEMENTS = [ diff --git a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/functions.ts b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/functions.ts new file mode 100644 index 000000000..df8c42f39 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/functions.ts @@ -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($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): 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; +} diff --git a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/pdf-viewer.service.ts b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/pdf-viewer.service.ts index 334e80f95..39bf9d168 100644 --- a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/pdf-viewer.service.ts +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/pdf-viewer.service.ts @@ -3,20 +3,23 @@ import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/w import { environment } from '@environments/environment'; import { BASE_HREF_FN } from '../../../../tokens'; 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 { 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 { ConfigService } from '@services/config.service'; import { NGXLogger } from 'ngx-logger'; import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, USELESS_ELEMENTS } from './constants'; import { Rgb } from '@shared/components/reusable-pdf-viewer/types'; 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 TextTool = Core.Tools.TextTool; import Annotation = Core.Annotations.Annotation; import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation; import DocumentViewer = Core.DocumentViewer; +import Color = Core.Annotations.Color; @Injectable({ providedIn: 'root', @@ -34,8 +37,10 @@ export class PdfViewer { loaded$: Observable; pageComplete$: Observable; + pageChanged$: Observable; compareMode$: Observable; totalPages$: Observable; + keyUp$: Observable; #instance: WebViewerInstance; readonly #compareMode$ = new BehaviorSubject(false); @@ -43,6 +48,7 @@ export class PdfViewer { constructor( private readonly _logger: NGXLogger, private readonly _activatedRoute: ActivatedRoute, + private readonly _annotationManager: REDAnnotationManager, private readonly _userPreferenceService: UserPreferenceService, private readonly _injector: Injector, ) {} @@ -81,8 +87,18 @@ export class PdfViewer { return this.isCompare ? Math.ceil(currentInternalPage / 2) : currentInternalPage; } + get #keyUp$() { + return fromEvent(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$() { - const layoutChanged$ = fromEvent(this.documentViewer, 'layoutChanged'); + const layoutChanged$ = fromEvent(this.documentViewer, 'layoutChanged').pipe(startWith('')); const pageCount$ = layoutChanged$.pipe( map(() => this.pageCount), distinctUntilChanged(), @@ -123,6 +139,11 @@ export class PdfViewer { return this.documentViewer.getCurrentPage(); } + get #pageChanged$() { + const page$ = fromEvent(this.documentViewer, 'pageNumberUpdated'); + return page$.pipe(tap(() => this._annotationManager.deselectAnnotations())); + } + navigateTo(page: string | number) { const parsedNumber = typeof page === 'string' ? parseInt(page, 10) : page; const paginationOffset = this.#paginationOffset; @@ -152,14 +173,34 @@ export class PdfViewer { this.loaded$ = merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast()); this.compareMode$ = this.#compareMode$.asObservable(); this.pageComplete$ = this.#pageComplete$.pipe(shareLast()); + this.pageChanged$ = this.#pageChanged$.pipe(shareLast()); this.totalPages$ = this.#totalPages$.pipe(shareDistinctLast()); + this.keyUp$ = this.#keyUp$; this.#setSelectionMode(); this.#configureElements(); this.#disableHotkeys(); + this.#clearSearchResultsWhenVisibilityChanged(); 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) { try { return this.documentViewer.getPageHeight(page); @@ -235,6 +276,23 @@ export class PdfViewer { 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() { this.#instance.UI.setFitMode('FitPage'); const displayModeManager = this.documentViewer.getDisplayModeManager(); diff --git a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/types.ts b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/types.ts index 42beedf9e..71407bb8c 100644 --- a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/types.ts +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/types.ts @@ -1,3 +1,6 @@ +import { Core } from '@pdftron/webviewer'; +import Annotation = Core.Annotations.Annotation; + export interface Rgb { readonly r: number; readonly g: number; @@ -10,3 +13,5 @@ export interface DeleteAnnotationsOptions { readonly isUndoRedo?: boolean; readonly source?: string; } + +export type AnnotationPredicate = (value: Annotation) => boolean;