diff --git a/lib/core/ng-package.json b/lib/core/ng-package.json index 1a536e6a39..244f97c25d 100644 --- a/lib/core/ng-package.json +++ b/lib/core/ng-package.json @@ -2,11 +2,6 @@ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", "dest": "../../dist/libs/core", "assets": [ - { - "glob": "pdf.worker.mjs", - "input": "./assets/pdfjs", - "output": "assets/pdfjs" - }, { "glob": "assets/**/*", "input": "./src/lib", @@ -38,7 +33,6 @@ "allowedNonPeerDependencies": [ "cropperjs", "angular-oauth2-oidc", - "date-fns", - "rxjs" + "date-fns" ] } diff --git a/lib/core/package.json b/lib/core/package.json index 73ead1cc4d..423ee3c28b 100644 --- a/lib/core/package.json +++ b/lib/core/package.json @@ -19,8 +19,7 @@ "dependencies": { "cropperjs": "^1.6.2", "angular-oauth2-oidc": "19.0.0", - "date-fns": "^2.30.0", - "rxjs": "7.8.2" + "date-fns": "^2.30.0" }, "peerDependencies": { "@angular/animations": ">=16.0.0", @@ -37,7 +36,13 @@ "@alfresco/js-api": ">=9.4.1", "@alfresco/adf-extensions": ">=8.4.1", "minimatch": ">=10.0.0", - "pdfjs-dist": ">=3.3.122" + "pdfjs-dist": ">=3.3.122", + "rxjs": ">=7.8.0" + }, + "peerDependenciesMeta": { + "pdfjs-dist": { + "optional": true + } }, "keywords": [ "core", diff --git a/lib/core/schematics/migrations/9_0_0/README.md b/lib/core/schematics/migrations/9_0_0/README.md new file mode 100644 index 0000000000..04c323c82a --- /dev/null +++ b/lib/core/schematics/migrations/9_0_0/README.md @@ -0,0 +1,91 @@ +# Migration Schematic: migrate-pdf-viewer-imports (v9.0.0) + +Automatically migrates consumer projects from the monolithic `@alfresco/adf-core` PDF viewer to the new `@alfresco/adf-core/viewer/pdf` secondary entrypoint. + +## What it does + +### 1. Rewrites imports + +Moves PDF-related symbols from `@alfresco/adf-core` to `@alfresco/adf-core/viewer/pdf`: + +**Before:** +```typescript +import { AppConfigService, PdfViewerComponent, RenderingQueueServices } from '@alfresco/adf-core'; +``` + +**After:** +```typescript +import { AppConfigService } from '@alfresco/adf-core'; +import { PdfViewerComponent, RenderingQueueServices } from '@alfresco/adf-core/viewer/pdf'; +``` + +Affected symbols: +- `PdfViewerComponent` +- `PdfPasswordDialogComponent` +- `PdfThumbListComponent` +- `PdfThumbComponent` +- `PDFJS_MODULE` +- `PDFJS_VIEWER_MODULE` +- `RenderingQueueServices` + +### 2. Adds `providePdfViewer()` to app providers + +The PDF viewer now requires explicit registration via the `providePdfViewer()` provider function. The schematic automatically adds it to the consumer's application configuration. + +**Standalone app (`app.config.ts`):** +```typescript +import { providePdfViewer } from '@alfresco/adf-core/viewer/pdf'; + +export const appConfig: ApplicationConfig = { + providers: [providePdfViewer(), /* ...existing providers */] +}; +``` + +**NgModule app (`app.module.ts`):** +```typescript +import { providePdfViewer } from '@alfresco/adf-core/viewer/pdf'; + +@NgModule({ + providers: [providePdfViewer()] +}) +export class AppModule {} +``` + +## How it runs + +Triggered automatically via `ng update @alfresco/adf-core` when upgrading to v9.0.0+. + +Can also be run manually: +```bash +ng generate @alfresco/adf-core:migrate-pdf-viewer-imports +``` + +## File discovery + +The schematic scans all `.ts` files in the project (excluding `node_modules`, `.git`, `.angular`, `.nxcache`) and processes any file containing one of the PDF symbols listed above. + +For provider injection, it searches for the app config file in this order: +1. `src/app/app.config.ts` +2. `src/app/app.module.ts` +3. `src/main.ts` +4. Falls back to scanning for any file containing `bootstrapApplication` or `ApplicationConfig` + +## Edge cases handled + +| Scenario | Behavior | +|----------|----------| +| Existing `@alfresco/adf-core/viewer/pdf` import | Merges moved symbols into it (deduplicates) | +| All symbols in the core import are PDF symbols | Removes the entire `@alfresco/adf-core` import | +| `providePdfViewer` already present in the project | Skips provider injection | +| No app config file found | Skips provider injection (consumer must add manually) | +| File has no PDF symbols | Skipped entirely (no modifications) | +| Empty providers array | Inserts `providePdfViewer()` as the first element | + +## Why this migration exists + +In v9.0.0, the PDF viewer was extracted into a secondary entrypoint (`@alfresco/adf-core/viewer/pdf`) so that: +- `pdfjs-dist` becomes an optional peer dependency +- Applications that don't use PDF viewing avoid bundling ~500KB of PDF processing code +- The primary `@alfresco/adf-core` bundle stays lean + +Without this schematic, consumers upgrading to v9 would see PDF files rendered as "unknown format" because the `PDF_VIEWER_COMPONENT` injection token defaults to `null` when `providePdfViewer()` is not called. diff --git a/lib/core/schematics/migrations/9_0_0/index.ts b/lib/core/schematics/migrations/9_0_0/index.ts new file mode 100644 index 0000000000..cf6ce30ccb --- /dev/null +++ b/lib/core/schematics/migrations/9_0_0/index.ts @@ -0,0 +1,274 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from 'typescript'; +import { Rule, Tree } from '@angular-devkit/schematics'; + +const PDF_SYMBOLS = [ + 'PdfViewerComponent', + 'PdfPasswordDialogComponent', + 'PdfThumbListComponent', + 'PdfThumbComponent', + 'PDFJS_MODULE', + 'PDFJS_VIEWER_MODULE', + 'RenderingQueueServices' +]; + +const OLD_SOURCE = '@alfresco/adf-core'; +const NEW_SOURCE = '@alfresco/adf-core/viewer/pdf'; + +/** + * @returns Schematic rule for migrating PDF viewer imports to the secondary entrypoint + */ +export function migratePdfViewerImports(): Rule { + return (tree: Tree) => { + let providerAdded = false; + + tree.visit((filePath: string) => { + if ( + !filePath.includes('/.git/') && + !filePath.includes('/node_modules/') && + !filePath.includes('/.angular/') && + !filePath.includes('/.nxcache/') && + /\.ts$/.test(filePath) + ) { + const bufferContent = tree.read(filePath); + if (!bufferContent) { + return; + } + + const fileContent = bufferContent.toString(); + + if (PDF_SYMBOLS.some((sym) => fileContent.includes(sym))) { + const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true); + const updatedContent = movePdfImports(sourceFile, fileContent); + + if (updatedContent !== fileContent) { + tree.overwrite(filePath, updatedContent); + } + } + + if (!providerAdded && fileContent.includes('providePdfViewer')) { + providerAdded = true; + } + } + }); + + if (!providerAdded) { + addProvidePdfViewerToAppConfig(tree); + } + + return tree; + }; +} + +/** + * @param sourceFile - the parsed TypeScript source file + * @param fileContent - the raw file content string + * @returns updated file content with PDF imports moved to the secondary entrypoint + */ +function movePdfImports(sourceFile: ts.SourceFile, fileContent: string): string { + const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration); + const coreImport = importDeclarations.find((decl) => { + const moduleSpecifier = decl.moduleSpecifier.getText().replace(/['"]/g, ''); + return moduleSpecifier === OLD_SOURCE; + }); + + if (!coreImport?.importClause?.namedBindings || !ts.isNamedImports(coreImport.importClause.namedBindings)) { + return fileContent; + } + + const namedImports = coreImport.importClause.namedBindings.elements; + const pdfImports = namedImports.filter((el) => PDF_SYMBOLS.includes(el.name.text)); + const remainingImports = namedImports.filter((el) => !PDF_SYMBOLS.includes(el.name.text)); + + if (pdfImports.length === 0) { + return fileContent; + } + + const pdfImportNames = pdfImports.map((el) => el.name.text); + + const existingNewImport = importDeclarations.find((decl) => decl.moduleSpecifier.getText().replace(/['"]/g, '') === NEW_SOURCE); + + const existingNewImportNames: string[] = []; + if (existingNewImport?.importClause?.namedBindings && ts.isNamedImports(existingNewImport.importClause.namedBindings)) { + existingNewImportNames.push(...existingNewImport.importClause.namedBindings.elements.map((el) => el.name.text)); + } + + const mergedNames = [...new Set([...existingNewImportNames, ...pdfImportNames])]; + const mergedImportStatement = `import { ${mergedNames.join(', ')} } from '${NEW_SOURCE}';`; + + const edits: { start: number; end: number; replacement: string }[] = []; + + if (remainingImports.length === 0) { + edits.push({ start: coreImport.getFullStart(), end: coreImport.getEnd(), replacement: '' }); + } else { + const remainingNames = remainingImports.map((el) => el.getText()).join(', '); + const updatedImport = `import { ${remainingNames} } from '${OLD_SOURCE}';`; + edits.push({ start: coreImport.getStart(), end: coreImport.getEnd(), replacement: updatedImport }); + } + + if (existingNewImport) { + edits.push({ start: existingNewImport.getStart(), end: existingNewImport.getEnd(), replacement: mergedImportStatement }); + } + + edits.sort((a, b) => b.start - a.start); + let updatedContent = fileContent; + for (const edit of edits) { + updatedContent = updatedContent.slice(0, edit.start) + edit.replacement + updatedContent.slice(edit.end); + } + + if (!existingNewImport) { + const updatedSource = ts.createSourceFile('temp.ts', updatedContent, ts.ScriptTarget.Latest, true); + const firstNonImport = updatedSource.statements.find((stmt) => !ts.isImportDeclaration(stmt)); + const insertPos = firstNonImport ? firstNonImport.getFullStart() : updatedContent.length; + updatedContent = updatedContent.slice(0, insertPos).trimEnd() + '\n' + mergedImportStatement + '\n' + updatedContent.slice(insertPos); + } + + return updatedContent.replace(/\n{3,}/g, '\n\n'); +} + +/** + * @param tree - the schematic file tree + */ +function addProvidePdfViewerToAppConfig(tree: Tree): void { + const candidates = ['src/app/app.config.ts', 'src/app/app.module.ts', 'src/main.ts']; + let targetPath: string | null = null; + + for (const candidate of candidates) { + if (tree.exists(`/${candidate}`)) { + targetPath = `/${candidate}`; + break; + } + } + + if (!targetPath) { + tree.visit((filePath: string) => { + if (targetPath) { + return; + } + if (!filePath.includes('/node_modules/') && !filePath.includes('/.git/') && /\.ts$/.test(filePath)) { + const content = tree.read(filePath)?.toString() ?? ''; + if (content.includes('bootstrapApplication') || content.includes('ApplicationConfig')) { + targetPath = filePath; + } + } + }); + } + + if (!targetPath) { + return; + } + + const buffer = tree.read(targetPath); + if (!buffer) { + return; + } + const content = buffer.toString(); + + if (content.includes('providePdfViewer')) { + return; + } + + const sourceFile = ts.createSourceFile(targetPath, content, ts.ScriptTarget.Latest, true); + const result = insertProvidePdfViewer(sourceFile, content); + + if (result !== content) { + tree.overwrite(targetPath, result); + } +} + +/** + * @param sourceFile - the parsed TypeScript source file + * @param content - the raw file content + * @returns updated content with providePdfViewer() added to providers + */ +function insertProvidePdfViewer(sourceFile: ts.SourceFile, content: string): string { + const providersArray = findProvidersArray(sourceFile); + if (!providersArray) { + return content; + } + + const lastElement = providersArray.elements[providersArray.elements.length - 1]; + let insertPos: number; + let prefix: string; + + if (lastElement) { + insertPos = lastElement.getEnd(); + prefix = ', providePdfViewer()'; + } else { + insertPos = providersArray.getStart() + 1; + prefix = 'providePdfViewer()'; + } + + let updatedContent = content.slice(0, insertPos) + prefix + content.slice(insertPos); + + const importStatement = `import { providePdfViewer } from '${NEW_SOURCE}';\n`; + const existingPdfImport = sourceFile.statements + .filter(ts.isImportDeclaration) + .find((decl) => decl.moduleSpecifier.getText().replace(/['"]/g, '') === NEW_SOURCE); + + if (existingPdfImport) { + if (!content.includes('providePdfViewer')) { + const updatedSource = ts.createSourceFile('temp.ts', updatedContent, ts.ScriptTarget.Latest, true); + const existingDecl = updatedSource.statements + .filter(ts.isImportDeclaration) + .find((decl) => decl.moduleSpecifier.getText().replace(/['"]/g, '') === NEW_SOURCE); + + if (existingDecl?.importClause?.namedBindings && ts.isNamedImports(existingDecl.importClause.namedBindings)) { + const names = existingDecl.importClause.namedBindings.elements.map((el) => el.name.text); + if (!names.includes('providePdfViewer')) { + names.push('providePdfViewer'); + const newImport = `import { ${names.join(', ')} } from '${NEW_SOURCE}';`; + updatedContent = updatedContent.slice(0, existingDecl.getStart()) + newImport + updatedContent.slice(existingDecl.getEnd()); + } + } + } + } else { + const firstImport = sourceFile.statements.find(ts.isImportDeclaration); + const importInsertPos = firstImport ? firstImport.getFullStart() : 0; + updatedContent = updatedContent.slice(0, importInsertPos) + importStatement + updatedContent.slice(importInsertPos); + } + + return updatedContent; +} + +/** + * @param sourceFile - the TypeScript source file to search + * @returns the providers ArrayLiteralExpression if found + */ +function findProvidersArray(sourceFile: ts.SourceFile): ts.ArrayLiteralExpression | null { + let result: ts.ArrayLiteralExpression | null = null; + + const visit = (node: ts.Node): void => { + if (result) { + return; + } + + if (ts.isPropertyAssignment(node) && node.name.getText() === 'providers' && ts.isArrayLiteralExpression(node.initializer)) { + result = node.initializer; + return; + } + + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + return result; +} + +export default migratePdfViewerImports; diff --git a/lib/core/schematics/migrations/collection.json b/lib/core/schematics/migrations/collection.json index 34e94b44cf..72d2c85091 100644 --- a/lib/core/schematics/migrations/collection.json +++ b/lib/core/schematics/migrations/collection.json @@ -5,6 +5,11 @@ "description": "Update alfresco-api imports", "version": "7.0.0", "factory": "./7_0_0/index#updateAlfrescoApiImports" + }, + "migrate-pdf-viewer-imports": { + "description": "Move PDF viewer imports to @alfresco/adf-core/viewer/pdf secondary entrypoint", + "version": "9.0.0", + "factory": "./9_0_0/index#migratePdfViewerImports" } }, "packageJsonUpdates": { diff --git a/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.html b/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.html index dd4d0f9729..be87ced881 100644 --- a/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.html +++ b/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.html @@ -32,17 +32,13 @@ } @case ('pdf') { - + @if (pdfViewerComponent) { + + } @else { + + } } @case ('image') { diff --git a/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.spec.ts b/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.spec.ts index 6c8a712342..221afd903d 100644 --- a/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.spec.ts +++ b/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.spec.ts @@ -18,13 +18,30 @@ import { AppExtensionService, ViewerExtensionRef } from '@alfresco/adf-extensions'; import { Location } from '@angular/common'; import { SpyLocation } from '@angular/common/testing'; -import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core'; +import { Component, DebugElement, EventEmitter, Input, Output, TemplateRef, ViewChild } from '@angular/core'; import { ComponentFixture, DeferBlockBehavior, TestBed } from '@angular/core/testing'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { UnitTestingUtils } from '../../../testing'; -import { RenderingQueueServices } from '../../services/rendering-queue.services'; import { ViewerRenderComponent } from './viewer-render.component'; -import { ImgViewerComponent, MediaPlayerComponent, PdfViewerComponent, ViewerExtensionDirective } from '@alfresco/adf-core'; +import { PDF_VIEWER_COMPONENT } from '../../tokens/pdf-viewer.token'; +import { PdfViewerRef } from '../../tokens/pdf-viewer-ref'; +import { ImgViewerComponent, MediaPlayerComponent, ViewerExtensionDirective } from '@alfresco/adf-core'; + +@Component({ + selector: 'adf-pdf-viewer', + template: '
' +}) +class MockPdfViewerComponent implements PdfViewerRef { + @Input() urlFile = ''; + @Input() blobFile: Blob; + @Input() fileName = ''; + @Input() allowThumbnails = false; + @Input() thumbnailsTemplate: TemplateRef = null; + @Input() cacheType = ''; + @Output() pagesLoaded = new EventEmitter(); + @Output() error = new EventEmitter(); + @Output() close = new EventEmitter(); +} @Component({ selector: 'adf-double-viewer', @@ -69,7 +86,7 @@ describe('ViewerComponent', () => { beforeEach(async () => { TestBed.configureTestingModule({ imports: [MatDialogModule, ViewerRenderComponent, DoubleViewerComponent], - providers: [RenderingQueueServices, { provide: Location, useClass: SpyLocation }, MatDialog], + providers: [{ provide: Location, useClass: SpyLocation }, MatDialog, { provide: PDF_VIEWER_COMPONENT, useValue: MockPdfViewerComponent }], deferBlockBehavior: DeferBlockBehavior.Playthrough }); fixture = TestBed.createComponent(ViewerRenderComponent); @@ -532,7 +549,7 @@ describe('ViewerComponent', () => { component.ngOnChanges(); fixture.detectChanges(); - const imgViewer = testingUtils.getByDirective(PdfViewerComponent); + const imgViewer = testingUtils.getByDirective(MockPdfViewerComponent); imgViewer.triggerEventHandler('pagesLoaded', null); fixture.detectChanges(); diff --git a/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.ts b/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.ts index a83c6b0302..32001f2b0d 100644 --- a/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.ts +++ b/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.ts @@ -16,9 +16,10 @@ */ import { AppExtensionService, ExtensionsModule, ViewerExtensionRef, PreviewExtensionComponent } from '@alfresco/adf-extensions'; -import { NgForOf, NgTemplateOutlet } from '@angular/common'; +import { NgComponentOutlet, NgForOf, NgTemplateOutlet } from '@angular/common'; import { Component, + effect, EventEmitter, Injector, Input, @@ -26,18 +27,22 @@ import { OnInit, Output, TemplateRef, + Type, ViewChild, ViewEncapsulation, - inject + inject, + viewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { TranslatePipe } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; import { Track } from '../../models/viewer.model'; import { ViewUtilService } from '../../services/view-util.service'; +import { PDF_VIEWER_COMPONENT } from '../../tokens/pdf-viewer.token'; +import { PdfViewerRef } from '../../tokens/pdf-viewer-ref'; import { ImgViewerComponent } from '../img-viewer/img-viewer.component'; import { MediaPlayerComponent } from '../media-player/media-player.component'; -import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'; import { TxtViewerComponent } from '../txt-viewer/txt-viewer.component'; import { UnknownFormatComponent } from '../unknown-format/unknown-format.component'; @@ -50,7 +55,7 @@ import { UnknownFormatComponent } from '../unknown-format/unknown-format.compone imports: [ TranslatePipe, MatProgressSpinnerModule, - PdfViewerComponent, + NgComponentOutlet, ImgViewerComponent, MediaPlayerComponent, TxtViewerComponent, @@ -68,6 +73,30 @@ export class ViewerRenderComponent implements OnChanges, OnInit { dialog = inject(MatDialog); readonly injector = inject(Injector); + readonly pdfViewerComponent: Type | null = inject(PDF_VIEWER_COMPONENT, { optional: true }); + + pdfViewerInputs: Record = {}; + + private readonly pdfOutlet = viewChild(NgComponentOutlet); + + constructor() { + effect((onCleanup) => { + const outlet = this.pdfOutlet(); + const instance = outlet?.componentInstance as PdfViewerRef | null; + if (!instance) { + return; + } + + const subs: Subscription[] = [ + instance.pagesLoaded.subscribe(() => this.markAsLoaded()), + instance.close.subscribe(() => this.onClose()), + instance.error.subscribe(() => this.onUnsupportedFile()) + ]; + + onCleanup(() => subs.forEach((s) => s.unsubscribe())); + }); + } + /** * If you want to load an external file that does not come from ACS you * can use this URL to specify where to load the file from. @@ -197,6 +226,13 @@ export class ViewerRenderComponent implements OnChanges, OnInit { } else if (this.urlFile) { this.setUpUrlFile(); } + if (this.viewerType === 'pdf' && !this.pdfViewerComponent) { + console.error( + '@alfresco/adf-core: PDF viewer is not configured. ' + + 'Add providePdfViewer() from @alfresco/adf-core/viewer/pdf to your application providers.' + ); + } + this.updatePdfViewerInputs(); } markAsLoaded() { @@ -250,4 +286,15 @@ export class ViewerRenderComponent implements OnChanges, OnInit { onClose() { this.close.next(true); } + + private updatePdfViewerInputs(): void { + this.pdfViewerInputs = { + urlFile: this.urlFile, + blobFile: this.blobFile, + fileName: this.internalFileName, + allowThumbnails: this.allowThumbnails, + thumbnailsTemplate: this.thumbnailsTemplate, + cacheType: this.cacheTypeForContent + }; + } } diff --git a/lib/core/src/lib/viewer/public-api.ts b/lib/core/src/lib/viewer/public-api.ts index 9d60324639..3d17c4f7a9 100644 --- a/lib/core/src/lib/viewer/public-api.ts +++ b/lib/core/src/lib/viewer/public-api.ts @@ -16,13 +16,10 @@ */ export * from './services/view-util.service'; +export * from './tokens/pdf-viewer.token'; +export * from './tokens/pdf-viewer-ref'; export * from './components/img-viewer/img-viewer.component'; export * from './components/media-player/media-player.component'; -export * from './components/pdf-viewer-password-dialog/pdf-viewer-password-dialog'; -export * from './components/pdf-viewer/pdf-viewer.component'; -export * from './components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component'; -export * from './components/pdf-viewer-thumb/pdf-viewer-thumb.component'; -export * from './components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component'; export * from './components/txt-viewer/txt-viewer.component'; export * from './components/unknown-format/unknown-format.component'; export * from './components/viewer-more-actions.component'; diff --git a/lib/core/src/lib/viewer/tokens/pdf-viewer-ref.ts b/lib/core/src/lib/viewer/tokens/pdf-viewer-ref.ts new file mode 100644 index 0000000000..965ffbd1fb --- /dev/null +++ b/lib/core/src/lib/viewer/tokens/pdf-viewer-ref.ts @@ -0,0 +1,30 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter, TemplateRef } from '@angular/core'; + +export interface PdfViewerRef { + urlFile: string; + blobFile: Blob; + fileName: string; + allowThumbnails: boolean; + thumbnailsTemplate: TemplateRef; + cacheType: string; + pagesLoaded: EventEmitter; + error: EventEmitter; + close: EventEmitter; +} diff --git a/lib/core/src/lib/viewer/tokens/pdf-viewer.token.ts b/lib/core/src/lib/viewer/tokens/pdf-viewer.token.ts new file mode 100644 index 0000000000..adb5e24064 --- /dev/null +++ b/lib/core/src/lib/viewer/tokens/pdf-viewer.token.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InjectionToken, Type } from '@angular/core'; +import { PdfViewerRef } from './pdf-viewer-ref'; + +export const PDF_VIEWER_COMPONENT = new InjectionToken | null>('PDF_VIEWER_COMPONENT', { + providedIn: 'root', + factory: () => null +}); diff --git a/lib/core/src/lib/viewer/viewer.module.ts b/lib/core/src/lib/viewer/viewer.module.ts index 46b5f45d6f..33bf8aea7c 100644 --- a/lib/core/src/lib/viewer/viewer.module.ts +++ b/lib/core/src/lib/viewer/viewer.module.ts @@ -19,10 +19,6 @@ import { NgModule } from '@angular/core'; import { DownloadPromptDialogComponent } from './components/download-prompt-dialog/download-prompt-dialog.component'; import { ImgViewerComponent } from './components/img-viewer/img-viewer.component'; import { MediaPlayerComponent } from './components/media-player/media-player.component'; -import { PdfPasswordDialogComponent } from './components/pdf-viewer-password-dialog/pdf-viewer-password-dialog'; -import { PdfThumbComponent } from './components/pdf-viewer-thumb/pdf-viewer-thumb.component'; -import { PdfThumbListComponent } from './components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component'; -import { PdfViewerComponent } from './components/pdf-viewer/pdf-viewer.component'; import { TxtViewerComponent } from './components/txt-viewer/txt-viewer.component'; import { UnknownFormatComponent } from './components/unknown-format/unknown-format.component'; import { ViewerMoreActionsComponent } from './components/viewer-more-actions.component'; @@ -36,14 +32,10 @@ import { ViewerComponent } from './components/viewer.component'; import { ViewerExtensionDirective } from './directives/viewer-extension.directive'; export const VIEWER_DIRECTIVES = [ - PdfPasswordDialogComponent, ViewerRenderComponent, ImgViewerComponent, TxtViewerComponent, MediaPlayerComponent, - PdfViewerComponent, - PdfThumbComponent, - PdfThumbListComponent, ViewerExtensionDirective, UnknownFormatComponent, ViewerToolbarComponent, diff --git a/lib/core/viewer/pdf/README.md b/lib/core/viewer/pdf/README.md new file mode 100644 index 0000000000..ec2c0805af --- /dev/null +++ b/lib/core/viewer/pdf/README.md @@ -0,0 +1,77 @@ +# @alfresco/adf-core/viewer/pdf + +Secondary entrypoint that provides the PDF viewer component, extracted from `@alfresco/adf-core` to make `pdfjs-dist` an optional dependency. + +## Setup + +1. Install `pdfjs-dist` (if not already present): + +```bash +npm install pdfjs-dist +``` + +2. Add `providePdfViewer()` to your app providers: + +```typescript +import { providePdfViewer } from '@alfresco/adf-core/viewer/pdf'; + +// Standalone app (app.config.ts) +export const appConfig: ApplicationConfig = { + providers: [ + providePdfViewer(), + // ...other providers + ] +}; + +// NgModule-based app (app.module.ts) +@NgModule({ + providers: [providePdfViewer()] +}) +export class AppModule {} +``` + +That's it. The viewer will render PDFs using the real `PdfViewerComponent`. + +## Migration from v8.x + +If upgrading from a version where `PdfViewerComponent` was part of `@alfresco/adf-core` directly, run: + +```bash +ng update @alfresco/adf-core +``` + +The migration schematic will: +- Rewrite imports of PDF symbols (`PdfViewerComponent`, `PdfPasswordDialogComponent`, `PdfThumbListComponent`, `PdfThumbComponent`, `PDFJS_MODULE`, `PDFJS_VIEWER_MODULE`, `RenderingQueueServices`) from `@alfresco/adf-core` to `@alfresco/adf-core/viewer/pdf` +- Add `providePdfViewer()` to your app's providers array + +## Opting out of PDF support + +If your application does not need to render PDFs, you can skip installing `pdfjs-dist` and omit `providePdfViewer()`. The viewer will display an "unknown format" placeholder for PDF files. + +## Exports + +| Symbol | Description | +|--------|-------------| +| `providePdfViewer()` | Provider function that registers the PDF viewer | +| `PdfViewerComponent` | The PDF viewer component | +| `PdfPasswordDialogComponent` | Password dialog for protected PDFs | +| `PdfThumbListComponent` | PDF thumbnail list | +| `PdfThumbComponent` | Individual PDF thumbnail | +| `PDFJS_MODULE` | Injection token for pdfjs-dist library | +| `PDFJS_VIEWER_MODULE` | Injection token for pdfjs viewer module | +| `RenderingQueueServices` | Service managing PDF page render queue | + +## Architecture + +``` +@alfresco/adf-core (primary entrypoint) + - Exports: PDF_VIEWER_COMPONENT token, PdfViewerRef interface + - ViewerRenderComponent injects the token via NgComponentOutlet + - Does NOT depend on pdfjs-dist + +@alfresco/adf-core/viewer/pdf (this entrypoint) + - Imports from: @alfresco/adf-core, pdfjs-dist + - Exports: PdfViewerComponent, providePdfViewer(), etc. +``` + +The dependency is one-way (secondary -> primary). The primary entrypoint never imports from this package, avoiding circular dependencies and keeping `pdfjs-dist` out of the main bundle. diff --git a/lib/core/viewer/pdf/ng-package.json b/lib/core/viewer/pdf/ng-package.json new file mode 100644 index 0000000000..16ec7433ad --- /dev/null +++ b/lib/core/viewer/pdf/ng-package.json @@ -0,0 +1,8 @@ +{ + "lib": { + "entryFile": "src/index.ts", + "styleIncludePaths": [ + "../../src/lib" + ] + } +} diff --git a/lib/core/viewer/pdf/src/index.ts b/lib/core/viewer/pdf/src/index.ts new file mode 100644 index 0000000000..dde81adbbc --- /dev/null +++ b/lib/core/viewer/pdf/src/index.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './lib/components/pdf-viewer/pdf-viewer.component'; +export * from './lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component'; +export * from './lib/components/pdf-viewer-thumb/pdf-viewer-thumb.component'; +export * from './lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog'; +export * from './lib/services/rendering-queue.services'; +export * from './lib/provide-pdf-viewer'; diff --git a/lib/core/viewer/pdf/src/lib/assets/fake-test-file.pdf b/lib/core/viewer/pdf/src/lib/assets/fake-test-file.pdf new file mode 100644 index 0000000000..4843af26d6 Binary files /dev/null and b/lib/core/viewer/pdf/src/lib/assets/fake-test-file.pdf differ diff --git a/lib/core/viewer/pdf/src/lib/assets/fake-test-password-file.pdf b/lib/core/viewer/pdf/src/lib/assets/fake-test-password-file.pdf new file mode 100644 index 0000000000..d34fba5e0c Binary files /dev/null and b/lib/core/viewer/pdf/src/lib/assets/fake-test-password-file.pdf differ diff --git a/lib/core/viewer/pdf/src/lib/components/mock/pdfjs-lib.mock.ts b/lib/core/viewer/pdf/src/lib/components/mock/pdfjs-lib.mock.ts new file mode 100644 index 0000000000..633899ece1 --- /dev/null +++ b/lib/core/viewer/pdf/src/lib/components/mock/pdfjs-lib.mock.ts @@ -0,0 +1,53 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const annotations = [ + { + subtype: 'Text', + name: 'NoIcon', + id: 'R13', + titleObj: { + str: 'Annotation title' + }, + contentsObj: { + str: 'Annotation contents' + }, + modificationDate: "D:20260202104106Z00'00", + popupRef: 'R1' + } +]; +export default { + GlobalWorkerOptions: {}, + getDocument: jasmine.createSpy('getDocument').and.callFake(() => ({ + loadingTask: () => ({ + destroy: () => Promise.resolve() + }), + promise: new Promise((resolve) => { + resolve({ + numPages: 6, + getPage: () => + Promise.resolve({ + getAnnotations: () => annotations + }) + }); + }) + })), + PasswordResponses: { + NEED_PASSWORD: 1, + INCORRECT_PASSWORD: 2 + } +}; diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.html b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.html similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.html rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.html diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.scss b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.scss similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.scss rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.scss diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.spec.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.spec.ts similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.spec.ts rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.spec.ts diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts similarity index 97% rename from lib/core/src/lib/viewer/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts index bd5899bb73..50509956b5 100644 --- a/lib/core/src/lib/viewer/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts +++ b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts @@ -23,7 +23,7 @@ import { ReactiveFormsModule, UntypedFormControl, Validators } from '@angular/fo import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { TranslatePipe } from '@ngx-translate/core'; -import { IconModule } from '../../../icon/icon.module'; +import { IconModule } from '@alfresco/adf-core'; declare const pdfjsLib: { PasswordResponses: { NEED_PASSWORD: number; INCORRECT_PASSWORD: number } }; diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-thumb/pdf-viewer-thumb.component.html b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumb/pdf-viewer-thumb.component.html similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer-thumb/pdf-viewer-thumb.component.html rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumb/pdf-viewer-thumb.component.html diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-thumb/pdf-viewer-thumb.component.spec.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumb/pdf-viewer-thumb.component.spec.ts similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer-thumb/pdf-viewer-thumb.component.spec.ts rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumb/pdf-viewer-thumb.component.spec.ts diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-thumb/pdf-viewer-thumb.component.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumb/pdf-viewer-thumb.component.ts similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer-thumb/pdf-viewer-thumb.component.ts rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumb/pdf-viewer-thumb.component.ts diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.html b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.html similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.html rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.html diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.scss b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.scss similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.scss rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.scss diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.spec.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.spec.ts similarity index 99% rename from lib/core/src/lib/viewer/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.spec.ts rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.spec.ts index a38fa7aed6..1a3826d2fd 100644 --- a/lib/core/src/lib/viewer/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.spec.ts +++ b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.spec.ts @@ -17,7 +17,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PdfThumbListComponent } from './pdf-viewer-thumbnails.component'; -import { UnitTestingUtils } from '../../../testing'; +import { UnitTestingUtils } from '@alfresco/adf-core'; import { DOWN_ARROW, ESCAPE, UP_ARROW } from '@angular/cdk/keycodes'; declare const pdfjsViewer: any; diff --git a/lib/core/src/lib/viewer/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.ts similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.ts rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component.ts diff --git a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer-host.component.scss b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer-host.component.scss similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer-host.component.scss rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer-host.component.scss diff --git a/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer-integration.spec.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer-integration.spec.ts new file mode 100644 index 0000000000..ccf8939d46 --- /dev/null +++ b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer-integration.spec.ts @@ -0,0 +1,99 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PDF_VIEWER_COMPONENT, ViewerRenderComponent, UnitTestingUtils } from '@alfresco/adf-core'; +import { PdfViewerComponent, PDFJS_MODULE, PDFJS_VIEWER_MODULE } from './pdf-viewer.component'; +import { providePdfViewer } from '../../provide-pdf-viewer'; +import pdfjsLibraryMock from '../mock/pdfjs-lib.mock'; + +describe('PdfViewer Integration with ViewerRenderComponent', () => { + let fixture: ComponentFixture; + let component: ViewerRenderComponent; + let testingUtils: UnitTestingUtils; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [ViewerRenderComponent], + providers: [ + providePdfViewer(), + { provide: PDFJS_MODULE, useValue: pdfjsLibraryMock }, + { provide: PDFJS_VIEWER_MODULE, useValue: class {} } + ] + }); + + fixture = TestBed.createComponent(ViewerRenderComponent); + component = fixture.componentInstance; + testingUtils = new UnitTestingUtils(fixture.debugElement); + }); + + afterEach(() => { + fixture.destroy(); + }); + + it('should provide PdfViewerComponent via token', () => { + const token = TestBed.inject(PDF_VIEWER_COMPONENT); + expect(token).toBe(PdfViewerComponent); + }); + + it('should render real PdfViewerComponent for PDF files', async () => { + component.urlFile = 'fake-test-file.pdf'; + component.ngOnChanges(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(component.viewerType).toBe('pdf'); + expect(testingUtils.getByCSS('adf-pdf-viewer')).not.toBeNull(); + }); + + it('should pass inputs to real PdfViewerComponent', async () => { + component.urlFile = 'fake-test-file.pdf'; + component.allowThumbnails = true; + component.ngOnChanges(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const pdfViewer = testingUtils.getByCSS('adf-pdf-viewer'); + expect(pdfViewer).not.toBeNull(); + expect(pdfViewer.componentInstance.urlFile).toBe('fake-test-file.pdf'); + expect(pdfViewer.componentInstance.allowThumbnails).toBe(true); + }); + + it('should show unknown format when token is not provided', async () => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [ViewerRenderComponent], + providers: [{ provide: PDF_VIEWER_COMPONENT, useValue: null }] + }); + + fixture = TestBed.createComponent(ViewerRenderComponent); + component = fixture.componentInstance; + testingUtils = new UnitTestingUtils(fixture.debugElement); + + component.urlFile = 'fake-test-file.pdf'; + component.ngOnChanges(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(component.viewerType).toBe('pdf'); + expect(testingUtils.getByCSS('adf-pdf-viewer')).toBeNull(); + expect(testingUtils.getByCSS('adf-viewer-unknown-format')).not.toBeNull(); + }); +}); diff --git a/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer-real-pdf.spec.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer-real-pdf.spec.ts new file mode 100644 index 0000000000..35d02168ad --- /dev/null +++ b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer-real-pdf.spec.ts @@ -0,0 +1,126 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { provideCoreAuthTesting } from '@alfresco/adf-core'; +import { PdfViewerComponent } from './pdf-viewer.component'; +import { RenderingQueueServices } from '../../services/rendering-queue.services'; +import * as pdfjsLib from 'pdfjs-dist/build/pdf.min.mjs'; + +const MINIMAL_PDF_BASE64 = + 'JVBERi0xLjcKCjEgMCBvYmogICUgZW50cnkgcG9pbnQKPDwKICAvVHlwZSAvQ2F0YWxvZwog' + + 'IC9QYWdlcyAyIDAgUgo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvVHlwZSAvUGFnZXMKICAv' + + 'TWVkaWFCb3ggWyAwIDAgMjAwIDIwMCBdCiAgL0NvdW50IDEKICAvS2lkcyBbIDMgMCBSIF0K' + + 'Pj4KZW5kb2JqCgozIDAgb2JqCjw8CiAgL1R5cGUgL1BhZ2UKICAvUGFyZW50IDIgMCBSCiAg' + + 'L1Jlc291cmNlcyA8PAogICAgL0ZvbnQgPDwKICAgICAgL0YxIDQgMCBSIAogICAgPj4KICA+' + + 'PgogIC9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKCjQgMCBvYmoKPDwKICAvVHlwZSAvRm9u' + + 'dAogIC9TdWJ0eXBlIC9UeXBlMQogIC9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2Jq' + + 'Cgo1IDAgb2JqICAlIHBhZ2UgY29udGVudAo8PAogIC9MZW5ndGggNDQKPj4Kc3RyZWFtCkJU' + + 'CjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIHdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVu' + + 'ZG9iagoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAwMDAwIG4g' + + 'CjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAxIDAw' + + 'MDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PAogIC9TaXplIDYKICAvUm9v' + + 'dCAxIDAgUgo+PgpzdGFydHhyZWYKNDkyCiUlRU9G'; + +/** @returns a Blob containing a minimal valid 1-page PDF */ +function createRealPdfBlob(): Blob { + const binaryString = atob(MINIMAL_PDF_BASE64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return new Blob([bytes], { type: 'application/pdf' }); +} + +@Component({ + imports: [PdfViewerComponent], + template: `` +}) +class RealPdfTestHostComponent { + @ViewChild(PdfViewerComponent, { static: true }) + pdfViewerComponent: PdfViewerComponent; + + blobFile = createRealPdfBlob(); +} + +/** + * @param predicate - condition to wait for + * @param timeout - max wait time in ms + * @returns a promise that resolves when the predicate returns true + */ +function waitFor(predicate: () => boolean, timeout = 5000): Promise { + const start = Date.now(); + return new Promise((resolve, reject) => { + const check = () => { + if (predicate()) { + resolve(); + } else if (Date.now() - start > timeout) { + reject(new Error('waitFor timed out')); + } else { + setTimeout(check, 50); + } + }; + check(); + }); +} + +describe('PdfViewerComponent with real pdfjs-dist', () => { + let fixture: ComponentFixture; + let component: PdfViewerComponent; + + beforeAll(async () => { + const workerModule = await import('pdfjs-dist/build/pdf.worker.min.mjs'); + (globalThis as any).pdfjsWorker = { WorkerMessageHandler: workerModule.WorkerMessageHandler }; + }); + + afterAll(() => { + delete (globalThis as any).pdfjsWorker; + }); + + beforeEach(async () => { + pdfjsLib.GlobalWorkerOptions.workerSrc = ''; + + TestBed.configureTestingModule({ + imports: [RealPdfTestHostComponent], + providers: [provideCoreAuthTesting(), { provide: MatDialog, useValue: { open: () => {} } }, RenderingQueueServices] + }); + + fixture = TestBed.createComponent(RealPdfTestHostComponent); + component = fixture.componentInstance.pdfViewerComponent; + + spyOn(component as any, 'setupPdfJsWorker').and.resolveTo(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + it('should load a real PDF blob and detect page count', async () => { + fixture.detectChanges(); + await waitFor(() => component.totalPages > 0); + expect(component.totalPages).toBe(1); + }); + + it('should emit pagesLoaded when a real PDF is rendered', async () => { + const pagesLoadedSpy = spyOn(component.pagesLoaded, 'emit'); + fixture.detectChanges(); + await waitFor(() => pagesLoadedSpy.calls.count() > 0); + expect(pagesLoadedSpy).toHaveBeenCalled(); + }); +}); diff --git a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.html b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.html similarity index 100% rename from lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.html rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.html diff --git a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.scss b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.scss similarity index 98% rename from lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.scss rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.scss index 767650a0d5..d959e43e20 100644 --- a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.scss +++ b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.scss @@ -1,4 +1,4 @@ -@use '../../../styles/mat-selectors' as ms; +@use 'styles/mat-selectors' as ms; .adf-pdf-viewer { width: 100%; diff --git a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.spec.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.spec.ts similarity index 99% rename from lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.spec.ts rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.spec.ts index 74084eb5ac..3ddc7ce33a 100644 --- a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.spec.ts +++ b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.spec.ts @@ -21,9 +21,7 @@ import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core import { MatDialog } from '@angular/material/dialog'; import { By } from '@angular/platform-browser'; import { of } from 'rxjs'; -import { AppConfigService } from '../../../app-config'; -import { EventMock } from '../../../mock'; -import { UnitTestingUtils, provideCoreAuthTesting } from '../../../testing'; +import { AppConfigService, EventMock, UnitTestingUtils, provideCoreAuthTesting } from '@alfresco/adf-core'; import { RenderingQueueServices } from '../../services/rendering-queue.services'; import { PdfThumbListComponent } from '../pdf-viewer-thumbnails/pdf-viewer-thumbnails.component'; import { PDFJS_MODULE, PDFJS_VIEWER_MODULE, PdfViewerComponent } from './pdf-viewer.component'; @@ -461,7 +459,7 @@ describe('Test PdfViewer - User interaction', () => { component.urlFile = './fake-test-file.pdf'; fixture.detectChanges(); - component.ngOnChanges({ + component.ngOnChanges({ urlFile: new SimpleChange(null, './fake-test-file.pdf', true) }); diff --git a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.ts similarity index 98% rename from lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.ts rename to lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.ts index df4ac9a15b..bc38bb7507 100644 --- a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.ts +++ b/lib/core/viewer/pdf/src/lib/components/pdf-viewer/pdf-viewer.component.ts @@ -40,8 +40,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { from, Subject, switchMap } from 'rxjs'; import { catchError } from 'rxjs/operators'; -import { AppConfigService } from '../../../app-config'; -import { ToolbarComponent, ToolbarDividerComponent } from '../../../toolbar'; +import { AppConfigService, IconModule, ToolbarComponent, ToolbarDividerComponent } from '@alfresco/adf-core'; import { RenderingQueueServices } from '../../services/rendering-queue.services'; import { PdfPasswordDialogComponent } from '../pdf-viewer-password-dialog/pdf-viewer-password-dialog'; import { PdfThumbListComponent } from '../pdf-viewer-thumbnails/pdf-viewer-thumbnails.component'; @@ -49,7 +48,6 @@ import * as pdfjsLib from 'pdfjs-dist/build/pdf.min.mjs'; import { PDFDateString } from 'pdfjs-dist/build/pdf.min.mjs'; import { EventBus, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs'; import { OnProgressParameters, PDFDocumentLoadingTask, PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist/types/src/display/api'; -import { IconModule } from '../../../icon/icon.module'; export type PdfScaleMode = 'init' | 'page-actual' | 'page-width' | 'page-height' | 'page-fit' | 'auto'; @@ -630,7 +628,7 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { * @param event.source.container.id - the container id */ onPageChange(event: PageChangingEvent) { - if (event.source && event.source.container.id === `${this.randomPdfId}-viewer-pdf-viewer`) { + if (event.source?.container.id === `${this.randomPdfId}-viewer-pdf-viewer`) { this.page = event.pageNumber; this.displayPage = event.pageNumber; } diff --git a/lib/core/viewer/pdf/src/lib/provide-pdf-viewer.ts b/lib/core/viewer/pdf/src/lib/provide-pdf-viewer.ts new file mode 100644 index 0000000000..68740dfa32 --- /dev/null +++ b/lib/core/viewer/pdf/src/lib/provide-pdf-viewer.ts @@ -0,0 +1,25 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Provider } from '@angular/core'; +import { PDF_VIEWER_COMPONENT } from '@alfresco/adf-core'; +import { PdfViewerComponent } from './components/pdf-viewer/pdf-viewer.component'; + +/** @returns Provider that registers the PdfViewerComponent for the viewer */ +export function providePdfViewer(): Provider { + return { provide: PDF_VIEWER_COMPONENT, useValue: PdfViewerComponent }; +} diff --git a/lib/core/src/lib/viewer/services/rendering-queue.services.ts b/lib/core/viewer/pdf/src/lib/services/rendering-queue.services.ts similarity index 100% rename from lib/core/src/lib/viewer/services/rendering-queue.services.ts rename to lib/core/viewer/pdf/src/lib/services/rendering-queue.services.ts diff --git a/lib/insights/ng-package.json b/lib/insights/ng-package.json index a5805e7386..bf17446cd1 100644 --- a/lib/insights/ng-package.json +++ b/lib/insights/ng-package.json @@ -21,9 +21,6 @@ ] }, "allowedNonPeerDependencies": [ - "@alfresco/adf-core", - "@alfresco/adf-content-services", - "@ngx-translate/core", "chart.js", "ng2-charts", "raphael" diff --git a/lib/insights/package.json b/lib/insights/package.json index c6cbfde300..54a5509d18 100644 --- a/lib/insights/package.json +++ b/lib/insights/package.json @@ -11,9 +11,6 @@ "url": "https://github.com/Alfresco/alfresco-ng2-components/issues" }, "dependencies": { - "@alfresco/adf-core": ">=8.4.1", - "@alfresco/adf-content-services": ">=8.4.1", - "@ngx-translate/core": ">=17.0.0", "chart.js": "^4.3.0", "ng2-charts": "^4.1.1", "raphael": ">=2.3.0" @@ -23,7 +20,10 @@ "@angular/compiler": ">=14.1.3", "@angular/core": ">=14.1.3", "@angular/forms": ">=14.1.3", - "@angular/material": ">=14.1.2" + "@angular/material": ">=14.1.2", + "@alfresco/adf-core": ">=8.4.1", + "@alfresco/adf-content-services": ">=8.4.1", + "@ngx-translate/core": ">=17.0.0" }, "keywords": [ "analytics", diff --git a/lib/process-services-cloud/ng-package.json b/lib/process-services-cloud/ng-package.json index 6b88db1399..cbec8864b3 100644 --- a/lib/process-services-cloud/ng-package.json +++ b/lib/process-services-cloud/ng-package.json @@ -26,8 +26,5 @@ "../core/src/lib" ] }, - "allowedNonPeerDependencies": [ - "@apollo/client", - "apollo-angular" - ] + "allowedNonPeerDependencies": [] } diff --git a/tsconfig.json b/tsconfig.json index 4ea511f3bd..97d311250c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ "@alfresco/adf-core/breadcrumbs": ["lib/core/breadcrumbs/src/index.ts"], "@alfresco/adf-core/feature-flags": ["lib/core/feature-flags/public-api.ts"], "@alfresco/adf-core/shell": ["lib/core/shell/src/index.ts"], + "@alfresco/adf-core/viewer/pdf": ["lib/core/viewer/pdf/src/index.ts"], "@alfresco/adf-extensions": ["lib/extensions/src/public-api.ts"], "@alfresco/adf-insights": ["lib/insights/src/public-api.ts"], "@alfresco/adf-process-services": ["lib/process-services/src/public-api.ts"],