mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-09 23:49:29 -06:00
Enhancement: pngx pdf viewer (#12043)
This commit is contained in:
@@ -86,7 +86,6 @@
|
|||||||
],
|
],
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"allowedCommonJsDependencies": [
|
"allowedCommonJsDependencies": [
|
||||||
"ng2-pdf-viewer",
|
|
||||||
"file-saver",
|
"file-saver",
|
||||||
"utif"
|
"utif"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ test('should show a mobile preview', async ({ page }) => {
|
|||||||
await page.setViewportSize({ width: 400, height: 1000 })
|
await page.setViewportSize({ width: 400, height: 1000 })
|
||||||
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
||||||
await page.getByRole('tab', { name: 'Preview' }).click()
|
await page.getByRole('tab', { name: 'Preview' }).click()
|
||||||
await page.waitForSelector('pdf-viewer')
|
await page.waitForSelector('pngx-pdf-viewer')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should show a list of notes', async ({ page }) => {
|
test('should show a list of notes', async ({ page }) => {
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ module.exports = {
|
|||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
...esmPreset.moduleNameMapper,
|
...esmPreset.moduleNameMapper,
|
||||||
'^src/(.*)': '<rootDir>/src/$1',
|
'^src/(.*)': '<rootDir>/src/$1',
|
||||||
|
'^pdfjs-dist/legacy/build/pdf\\.mjs$':
|
||||||
|
'<rootDir>/src/test/mocks/pdfjs-legacy-build-pdf.ts',
|
||||||
|
'^pdfjs-dist/web/pdf_viewer\\.mjs$':
|
||||||
|
'<rootDir>/src/test/mocks/pdfjs-web-pdf_viewer.ts',
|
||||||
},
|
},
|
||||||
workerIdleMemoryLimit: '512MB',
|
workerIdleMemoryLimit: '512MB',
|
||||||
reporters: [
|
reporters: [
|
||||||
|
|||||||
@@ -27,12 +27,12 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"mime-names": "^1.0.0",
|
"mime-names": "^1.0.0",
|
||||||
"ng2-pdf-viewer": "^10.4.0",
|
|
||||||
"ngx-bootstrap-icons": "^1.9.3",
|
"ngx-bootstrap-icons": "^1.9.3",
|
||||||
"ngx-color": "^10.1.0",
|
"ngx-color": "^10.1.0",
|
||||||
"ngx-cookie-service": "^21.1.0",
|
"ngx-cookie-service": "^21.1.0",
|
||||||
"ngx-device-detector": "^11.0.0",
|
"ngx-device-detector": "^11.0.0",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^18.0.0",
|
"ngx-ui-tour-ng-bootstrap": "^18.0.0",
|
||||||
|
"pdfjs-dist": "^5.4.624",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
|
|||||||
157
src-ui/pnpm-lock.yaml
generated
157
src-ui/pnpm-lock.yaml
generated
@@ -56,9 +56,6 @@ importers:
|
|||||||
mime-names:
|
mime-names:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
ng2-pdf-viewer:
|
|
||||||
specifier: ^10.4.0
|
|
||||||
version: 10.4.0
|
|
||||||
ngx-bootstrap-icons:
|
ngx-bootstrap-icons:
|
||||||
specifier: ^1.9.3
|
specifier: ^1.9.3
|
||||||
version: 1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))
|
version: 1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))
|
||||||
@@ -74,6 +71,9 @@ importers:
|
|||||||
ngx-ui-tour-ng-bootstrap:
|
ngx-ui-tour-ng-bootstrap:
|
||||||
specifier: ^18.0.0
|
specifier: ^18.0.0
|
||||||
version: 18.0.0(2a89effa12f6df8cde064aa7713e7e29)
|
version: 18.0.0(2a89effa12f6df8cde064aa7713e7e29)
|
||||||
|
pdfjs-dist:
|
||||||
|
specifier: ^5.4.624
|
||||||
|
version: 5.4.624
|
||||||
rxjs:
|
rxjs:
|
||||||
specifier: ^7.8.2
|
specifier: ^7.8.2
|
||||||
version: 7.8.2
|
version: 7.8.2
|
||||||
@@ -2130,6 +2130,76 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.90':
|
||||||
|
resolution: {integrity: sha512-3JBULVF+BIgr7yy7Rf8UjfbkfFx4CtXrkJFD1MDgKJ83b56o0U9ciT8ZGTCNmwWkzu8RbNKlyqPP3KYRG88y7Q==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.90':
|
||||||
|
resolution: {integrity: sha512-L8XVTXl+8vd8u7nPqcX77NyG5RuFdVsJapQrKV9WE3jBayq1aSMht/IH7Dwiz/RNJ86E5ZSg9pyUPFIlx52PZA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.90':
|
||||||
|
resolution: {integrity: sha512-h0ukhlnGhacbn798VWYTQZpf6JPDzQYaow+vtQ2Fat7j7ImDdpg6tfeqvOTO1r8wS+s+VhBIFITC7aA1Aik0ZQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.90':
|
||||||
|
resolution: {integrity: sha512-JCvTl99b/RfdBtgftqrf+5UNF7GIbp7c5YBFZ+Bd6++4Y3phaXG/4vD9ZcF1bw1P4VpALagHmxvodHuQ9/TfTg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.90':
|
||||||
|
resolution: {integrity: sha512-vbWFp8lrP8NIM5L4zNOwnsqKIkJo0+GIRUDcLFV9XEJCptCc1FY6/tM02PT7GN4PBgochUPB1nBHdji6q3ieyQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.90':
|
||||||
|
resolution: {integrity: sha512-8Bc0BgGEeOaux4EfIfNzcRRw0JE+lO9v6RWQFCJNM9dJFE4QJffTf88hnmbOaI6TEMpgWOKipbha3dpIdUqb/g==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.90':
|
||||||
|
resolution: {integrity: sha512-0iiVDG5IH+gJb/YUrY/pRdbsjcgvwUmeckL/0gShWAA7004ygX2ST69M1wcfyxXrzFYjdF8S/Sn6aCAeBi89XQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.90':
|
||||||
|
resolution: {integrity: sha512-SkKmlHMvA5spXuKfh7p6TsScDf7lp5XlMbiUhjdCtWdOS6Qke/A4qGVOciy6piIUCJibL+YX+IgdGqzm2Mpx/w==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.90':
|
||||||
|
resolution: {integrity: sha512-o6QgS10gAS4vvELGDOOWYfmERXtkVRYFWBCjomILWfMgCvBVutn8M97fsMW5CrEuJI8YuxuJ7U+/DQ9oG93vDA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.90':
|
||||||
|
resolution: {integrity: sha512-2UHO/DC1oyuSjeCAhHA0bTD9qsg58kknRqjJqRfvIEFtdqdtNTcWXMCT9rQCuJ8Yx5ldhyh2SSp7+UDqD2tXZQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.90':
|
||||||
|
resolution: {integrity: sha512-48CxEbzua5BP4+OumSZdi3+9fNiRO8cGNBlO2bKwx1PoyD1R2AXzPtqd/no1f1uSl0W2+ihOO1v3pqT3USbmgQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.90':
|
||||||
|
resolution: {integrity: sha512-vO9j7TfwF9qYCoTOPO39yPLreTRslBVOaeIwhDZkizDvBb0MounnTl0yeWUMBxP4Pnkg9Sv+3eQwpxNUmTwt0w==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/nice-android-arm-eabi@1.1.1':
|
'@napi-rs/nice-android-arm-eabi@1.1.1':
|
||||||
resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==}
|
resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -5015,9 +5085,6 @@ packages:
|
|||||||
neo-async@2.6.2:
|
neo-async@2.6.2:
|
||||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||||
|
|
||||||
ng2-pdf-viewer@10.4.0:
|
|
||||||
resolution: {integrity: sha512-TPh1oLZoeARggreTG60Sl3ikSn+Z3+At9pLZ0o/vxPjc7mW2ok2XPyl2Oqz7VyP80ipVorldm1hsLPBmNe2zzA==}
|
|
||||||
|
|
||||||
ngx-bootstrap-icons@1.9.3:
|
ngx-bootstrap-icons@1.9.3:
|
||||||
resolution: {integrity: sha512-UsFqJ/cn0u5W39hVMIDbm+ze1dCF9fDV839scqeimi70Efcmg41zOx6GgR6i2gWAVFR0OBso1cdqb4E75XhTSw==}
|
resolution: {integrity: sha512-UsFqJ/cn0u5W39hVMIDbm+ze1dCF9fDV839scqeimi70Efcmg41zOx6GgR6i2gWAVFR0OBso1cdqb4E75XhTSw==}
|
||||||
engines: {node: '>= 16.18.1', npm: '>= 8.11.0'}
|
engines: {node: '>= 16.18.1', npm: '>= 8.11.0'}
|
||||||
@@ -5084,6 +5151,9 @@ packages:
|
|||||||
node-int64@0.4.0:
|
node-int64@0.4.0:
|
||||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||||
|
|
||||||
|
node-readable-to-web-readable-stream@0.4.2:
|
||||||
|
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
|
||||||
|
|
||||||
node-releases@2.0.27:
|
node-releases@2.0.27:
|
||||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||||
|
|
||||||
@@ -5279,13 +5349,9 @@ packages:
|
|||||||
path-to-regexp@8.3.0:
|
path-to-regexp@8.3.0:
|
||||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
||||||
|
|
||||||
path2d@0.2.2:
|
pdfjs-dist@5.4.624:
|
||||||
resolution: {integrity: sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==}
|
resolution: {integrity: sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=20.16.0 || >=22.3.0'}
|
||||||
|
|
||||||
pdfjs-dist@4.8.69:
|
|
||||||
resolution: {integrity: sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
@@ -8796,6 +8862,54 @@ snapshots:
|
|||||||
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
|
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.90':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.90':
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas-android-arm64': 0.1.90
|
||||||
|
'@napi-rs/canvas-darwin-arm64': 0.1.90
|
||||||
|
'@napi-rs/canvas-darwin-x64': 0.1.90
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.90
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.90
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.90
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.90
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.90
|
||||||
|
'@napi-rs/canvas-linux-x64-musl': 0.1.90
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc': 0.1.90
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.90
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/nice-android-arm-eabi@1.1.1':
|
'@napi-rs/nice-android-arm-eabi@1.1.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -11975,11 +12089,6 @@ snapshots:
|
|||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
|
|
||||||
ng2-pdf-viewer@10.4.0:
|
|
||||||
dependencies:
|
|
||||||
pdfjs-dist: 4.8.69
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
ngx-bootstrap-icons@1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0)):
|
ngx-bootstrap-icons@1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular/common': 21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2)
|
'@angular/common': 21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2)
|
||||||
@@ -12060,6 +12169,9 @@ snapshots:
|
|||||||
|
|
||||||
node-int64@0.4.0: {}
|
node-int64@0.4.0: {}
|
||||||
|
|
||||||
|
node-readable-to-web-readable-stream@0.4.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
nopt@9.0.0:
|
nopt@9.0.0:
|
||||||
@@ -12291,13 +12403,10 @@ snapshots:
|
|||||||
|
|
||||||
path-to-regexp@8.3.0: {}
|
path-to-regexp@8.3.0: {}
|
||||||
|
|
||||||
path2d@0.2.2:
|
pdfjs-dist@5.4.624:
|
||||||
optional: true
|
|
||||||
|
|
||||||
pdfjs-dist@4.8.69:
|
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
canvas: 3.0.0
|
'@napi-rs/canvas': 0.1.90
|
||||||
path2d: 0.2.2
|
node-readable-to-web-readable-stream: 0.4.2
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
|
|||||||
@@ -100,10 +100,10 @@ const mock = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(window, 'open', { value: jest.fn() })
|
Object.defineProperty(globalThis, 'open', { value: jest.fn() })
|
||||||
Object.defineProperty(window, 'localStorage', { value: mock() })
|
Object.defineProperty(globalThis, 'localStorage', { value: mock() })
|
||||||
Object.defineProperty(window, 'sessionStorage', { value: mock() })
|
Object.defineProperty(globalThis, 'sessionStorage', { value: mock() })
|
||||||
Object.defineProperty(window, 'getComputedStyle', {
|
Object.defineProperty(globalThis, 'getComputedStyle', {
|
||||||
value: () => ['-webkit-appearance'],
|
value: () => ['-webkit-appearance'],
|
||||||
})
|
})
|
||||||
Object.defineProperty(navigator, 'clipboard', {
|
Object.defineProperty(navigator, 'clipboard', {
|
||||||
@@ -115,13 +115,33 @@ Object.defineProperty(navigator, 'canShare', { value: () => true })
|
|||||||
if (!navigator.share) {
|
if (!navigator.share) {
|
||||||
Object.defineProperty(navigator, 'share', { value: jest.fn() })
|
Object.defineProperty(navigator, 'share', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
if (!URL.createObjectURL) {
|
if (!globalThis.URL.createObjectURL) {
|
||||||
Object.defineProperty(window.URL, 'createObjectURL', { value: jest.fn() })
|
Object.defineProperty(globalThis.URL, 'createObjectURL', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
if (!URL.revokeObjectURL) {
|
if (!globalThis.URL.revokeObjectURL) {
|
||||||
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
|
Object.defineProperty(globalThis.URL, 'revokeObjectURL', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
class MockResizeObserver {
|
||||||
|
private readonly callback: ResizeObserverCallback
|
||||||
|
|
||||||
|
constructor(callback: ResizeObserverCallback) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
observe = jest.fn()
|
||||||
|
unobserve = jest.fn()
|
||||||
|
disconnect = jest.fn()
|
||||||
|
|
||||||
|
trigger = (entries: ResizeObserverEntry[] = []) => {
|
||||||
|
this.callback(entries, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'ResizeObserver', {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: MockResizeObserver,
|
||||||
|
})
|
||||||
|
|
||||||
if (typeof IntersectionObserver === 'undefined') {
|
if (typeof IntersectionObserver === 'undefined') {
|
||||||
class MockIntersectionObserver {
|
class MockIntersectionObserver {
|
||||||
@@ -136,7 +156,7 @@ if (typeof IntersectionObserver === 'undefined') {
|
|||||||
takeRecords = jest.fn()
|
takeRecords = jest.fn()
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(window, 'IntersectionObserver', {
|
Object.defineProperty(globalThis, 'IntersectionObserver', {
|
||||||
writable: true,
|
writable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: MockIntersectionObserver,
|
value: MockIntersectionObserver,
|
||||||
|
|||||||
@@ -222,8 +222,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<select class="form-select" formControlName="pdfViewerDefaultZoom">
|
<select class="form-select" formControlName="pdfViewerDefaultZoom">
|
||||||
<option [ngValue]="ZoomSetting.PageWidth" i18n>Fit width</option>
|
<option [ngValue]="PdfZoomScale.PageWidth" i18n>Fit width</option>
|
||||||
<option [ngValue]="ZoomSetting.PageFit" i18n>Fit page</option>
|
<option [ngValue]="PdfZoomScale.PageFit" i18n>Fit page</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
|
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ import { PermissionsUserComponent } from '../../common/input/permissions/permiss
|
|||||||
import { SelectComponent } from '../../common/input/select/select.component'
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode'
|
import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode'
|
||||||
|
import { PdfZoomScale } from '../../common/pdf-viewer/pdf-viewer.types'
|
||||||
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||||
import { ZoomSetting } from '../../document-detail/zoom-setting'
|
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
enum SettingsNavIDs {
|
enum SettingsNavIDs {
|
||||||
@@ -196,7 +196,7 @@ export class SettingsComponent
|
|||||||
|
|
||||||
public readonly GlobalSearchType = GlobalSearchType
|
public readonly GlobalSearchType = GlobalSearchType
|
||||||
|
|
||||||
public readonly ZoomSetting = ZoomSetting
|
public readonly PdfZoomScale = PdfZoomScale
|
||||||
|
|
||||||
public readonly PdfEditorEditMode = PdfEditorEditMode
|
public readonly PdfEditorEditMode = PdfEditorEditMode
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<pdf-viewer [src]="pdfSrc" [render-text]="false" zoom="0.4" (after-load-complete)="pdfLoaded($event)"></pdf-viewer>
|
<pngx-pdf-viewer class="visually-hidden" [src]="pdfSrc" [renderMode]="PdfRenderMode.Single" [page]="1" [selectable]="false" (afterLoadComplete)="pdfLoaded($event)"></pngx-pdf-viewer>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">{{ title }}</h4>
|
<h4 class="modal-title">{{ title }}</h4>
|
||||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<span class="placeholder w-100 h-100"></span>
|
<span class="placeholder w-100 h-100"></span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<pdf-viewer class="fade" [class.show]="p.loaded" [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [original-size]="false" [show-all]="false" [render-text]="false" (page-rendered)="p.loaded = true"></pdf-viewer>
|
<pngx-pdf-viewer class="fade" [class.show]="p.loaded" [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [renderMode]="PdfRenderMode.Single" (rendered)="p.loaded = true"></pngx-pdf-viewer>
|
||||||
} @placeholder {
|
} @placeholder {
|
||||||
<div class="placeholder-glow w-100 h-100 z-10">
|
<div class="placeholder-glow w-100 h-100 z-10">
|
||||||
<span class="placeholder w-100 h-100"></span>
|
<span class="placeholder w-100 h-100"></span>
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
background-color: gray;
|
background-color: gray;
|
||||||
height: 240px;
|
height: 240px;
|
||||||
|
|
||||||
pdf-viewer {
|
pngx-pdf-viewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .ng2-pdf-viewer-container {
|
::ng-deep .pngx-pdf-viewer-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ import {
|
|||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
|
||||||
|
import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
|
||||||
|
import {
|
||||||
|
PdfRenderMode,
|
||||||
|
PngxPdfDocumentProxy,
|
||||||
|
} from '../pdf-viewer/pdf-viewer.types'
|
||||||
import { PdfEditorEditMode } from './pdf-editor-edit-mode'
|
import { PdfEditorEditMode } from './pdf-editor-edit-mode'
|
||||||
|
|
||||||
interface PageOperation {
|
interface PageOperation {
|
||||||
@@ -29,11 +33,12 @@ interface PageOperation {
|
|||||||
imports: [
|
imports: [
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
PdfViewerModule,
|
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
PngxPdfViewerComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PDFEditorComponent extends ConfirmDialogComponent {
|
export class PDFEditorComponent extends ConfirmDialogComponent {
|
||||||
|
PdfRenderMode = PdfRenderMode
|
||||||
public PdfEditorEditMode = PdfEditorEditMode
|
public PdfEditorEditMode = PdfEditorEditMode
|
||||||
|
|
||||||
private documentService = inject(DocumentService)
|
private documentService = inject(DocumentService)
|
||||||
@@ -53,7 +58,7 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
|
|||||||
return this.documentService.getPreviewUrl(this.documentID)
|
return this.documentService.getPreviewUrl(this.documentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfLoaded(pdf: PDFDocumentProxy) {
|
pdfLoaded(pdf: PngxPdfDocumentProxy) {
|
||||||
this.totalPages = pdf.numPages
|
this.totalPages = pdf.numPages
|
||||||
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
|
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
|
||||||
page: i + 1,
|
page: i + 1,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div #container class="pngx-pdf-viewer-container">
|
||||||
|
<div #viewer class="pdfViewer"></div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pngx-pdf-viewer-container {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer {
|
||||||
|
--scale-factor: 1;
|
||||||
|
--page-bg-color: unset;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer .page {
|
||||||
|
--user-unit: 1;
|
||||||
|
--total-scale-factor: calc(var(--scale-factor) * var(--user-unit));
|
||||||
|
--scale-round-x: 1px;
|
||||||
|
--scale-round-y: 1px;
|
||||||
|
direction: ltr;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
border: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
background-clip: content-box;
|
||||||
|
background-color: var(--page-bg-color, rgb(255 255 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer > .page:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer.singlePageView {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer.singlePageView .page {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer .canvasWrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .pdfViewer .canvasWrapper canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
contain: content;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer {
|
||||||
|
position: absolute;
|
||||||
|
text-align: initial;
|
||||||
|
inset: 0;
|
||||||
|
overflow: clip;
|
||||||
|
opacity: 1;
|
||||||
|
line-height: 1;
|
||||||
|
text-size-adjust: none;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
caret-color: CanvasText;
|
||||||
|
z-index: 0;
|
||||||
|
user-select: text;
|
||||||
|
--min-font-size: 1;
|
||||||
|
--text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size));
|
||||||
|
--min-font-size-inv: calc(1 / var(--min-font-size));
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer.highlighting {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer :is(span, br) {
|
||||||
|
position: absolute;
|
||||||
|
white-space: pre;
|
||||||
|
color: transparent;
|
||||||
|
cursor: text;
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer > :not(.markedContent),
|
||||||
|
:host ::ng-deep .textLayer .markedContent span:not(.markedContent) {
|
||||||
|
z-index: 1;
|
||||||
|
--font-height: 0;
|
||||||
|
font-size: calc(var(--text-scale-factor) * var(--font-height));
|
||||||
|
--scale-x: 1;
|
||||||
|
--rotate: 0deg;
|
||||||
|
transform: rotate(var(--rotate)) scaleX(var(--scale-x))
|
||||||
|
scale(var(--min-font-size-inv));
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer .markedContent {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer span[role='img'] {
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer .highlight {
|
||||||
|
--highlight-bg-color: rgb(180 0 170 / 0.25);
|
||||||
|
--highlight-selected-bg-color: rgb(0 100 0 / 0.25);
|
||||||
|
--highlight-backdrop-filter: none;
|
||||||
|
--highlight-selected-backdrop-filter: none;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 1px;
|
||||||
|
background-color: var(--highlight-bg-color);
|
||||||
|
backdrop-filter: var(--highlight-backdrop-filter);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .appended:is(.textLayer .highlight) {
|
||||||
|
position: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .begin:is(.textLayer .highlight) {
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .end:is(.textLayer .highlight) {
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .middle:is(.textLayer .highlight) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .selected:is(.textLayer .highlight) {
|
||||||
|
background-color: var(--highlight-selected-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .textLayer ::selection {
|
||||||
|
background: rgba(30, 100, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .annotationLayer {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import { SimpleChange } from '@angular/core'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
|
||||||
|
import { PDFSinglePageViewer, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs'
|
||||||
|
import { PngxPdfViewerComponent } from './pdf-viewer.component'
|
||||||
|
import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
|
||||||
|
|
||||||
|
describe('PngxPdfViewerComponent', () => {
|
||||||
|
let fixture: ComponentFixture<PngxPdfViewerComponent>
|
||||||
|
let component: PngxPdfViewerComponent
|
||||||
|
|
||||||
|
const initComponent = async (src = 'test.pdf') => {
|
||||||
|
component.src = src
|
||||||
|
fixture.detectChanges()
|
||||||
|
await fixture.whenStable()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PngxPdfViewerComponent],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PngxPdfViewerComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads a document and emits events', async () => {
|
||||||
|
const loadSpy = jest.fn()
|
||||||
|
const renderedSpy = jest.fn()
|
||||||
|
component.afterLoadComplete.subscribe(loadSpy)
|
||||||
|
component.rendered.subscribe(renderedSpy)
|
||||||
|
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(
|
||||||
|
'/assets/js/pdf.worker.min.mjs'
|
||||||
|
)
|
||||||
|
const isVisible = (component as any).findController.onIsPageVisible as
|
||||||
|
| (() => boolean)
|
||||||
|
| undefined
|
||||||
|
expect(isVisible?.()).toBe(true)
|
||||||
|
expect(loadSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ numPages: 1 })
|
||||||
|
)
|
||||||
|
expect(renderedSpy).toHaveBeenCalled()
|
||||||
|
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes single-page viewer and disables text layer', async () => {
|
||||||
|
component.renderMode = PdfRenderMode.Single
|
||||||
|
component.selectable = false
|
||||||
|
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as PDFSinglePageViewer & {
|
||||||
|
options: Record<string, unknown>
|
||||||
|
}
|
||||||
|
expect(viewer).toBeInstanceOf(PDFSinglePageViewer)
|
||||||
|
expect(viewer.options.textLayerMode).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies zoom, rotation, and page changes', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const pageSpy = jest.fn()
|
||||||
|
component.pageChange.subscribe(pageSpy)
|
||||||
|
|
||||||
|
component.zoomScale = PdfZoomScale.PageFit
|
||||||
|
component.zoom = PdfZoomLevel.Two
|
||||||
|
component.rotation = 90
|
||||||
|
component.page = 2
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
zoomScale: new SimpleChange(
|
||||||
|
PdfZoomScale.PageWidth,
|
||||||
|
PdfZoomScale.PageFit,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
zoom: new SimpleChange(PdfZoomLevel.One, PdfZoomLevel.Two, false),
|
||||||
|
rotation: new SimpleChange(undefined, 90, false),
|
||||||
|
page: new SimpleChange(undefined, 2, false),
|
||||||
|
})
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as PDFViewer
|
||||||
|
expect(viewer.pagesRotation).toBe(90)
|
||||||
|
expect(viewer.currentPageNumber).toBe(2)
|
||||||
|
expect(pageSpy).toHaveBeenCalledWith(2)
|
||||||
|
|
||||||
|
viewer.currentScale = 1
|
||||||
|
;(component as any).applyScale()
|
||||||
|
expect(viewer.currentScaleValue).toBe(PdfZoomScale.PageFit)
|
||||||
|
expect(viewer.currentScale).toBe(2)
|
||||||
|
|
||||||
|
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
|
||||||
|
component.page = 2
|
||||||
|
;(component as any).lastViewerPage = 2
|
||||||
|
;(component as any).applyViewerState()
|
||||||
|
expect((component as any).lastViewerPage).toBeUndefined()
|
||||||
|
expect(applyScaleSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dispatches find when search query changes after render', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
|
||||||
|
const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
|
||||||
|
|
||||||
|
;(component as any).hasRenderedPage = true
|
||||||
|
component.searchQuery = 'needle'
|
||||||
|
component.ngOnChanges({
|
||||||
|
searchQuery: new SimpleChange('', 'needle', false),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(dispatchSpy).toHaveBeenCalledWith('find', {
|
||||||
|
query: 'needle',
|
||||||
|
caseSensitive: false,
|
||||||
|
highlightAll: true,
|
||||||
|
phraseSearch: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
searchQuery: new SimpleChange('needle', 'needle', false),
|
||||||
|
})
|
||||||
|
expect(dispatchSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits error when document load fails', async () => {
|
||||||
|
const errorSpy = jest.fn()
|
||||||
|
component.loadError.subscribe(errorSpy)
|
||||||
|
|
||||||
|
jest.spyOn(pdfjs, 'getDocument').mockImplementationOnce(() => {
|
||||||
|
return {
|
||||||
|
promise: Promise.reject(new Error('boom')),
|
||||||
|
destroy: jest.fn(),
|
||||||
|
} as any
|
||||||
|
})
|
||||||
|
|
||||||
|
await initComponent('bad.pdf')
|
||||||
|
|
||||||
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cleans up resources on destroy', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as { cleanup: jest.Mock }
|
||||||
|
const loadingTask = (component as any).loadingTask as unknown as {
|
||||||
|
destroy: jest.Mock
|
||||||
|
}
|
||||||
|
const resizeObserver = (component as any).resizeObserver as unknown as {
|
||||||
|
disconnect: jest.Mock
|
||||||
|
}
|
||||||
|
const eventBus = (component as any).eventBus as { off: jest.Mock }
|
||||||
|
|
||||||
|
jest.spyOn(viewer, 'cleanup')
|
||||||
|
jest.spyOn(loadingTask, 'destroy')
|
||||||
|
jest.spyOn(resizeObserver, 'disconnect')
|
||||||
|
jest.spyOn(eventBus, 'off')
|
||||||
|
|
||||||
|
component.ngOnDestroy()
|
||||||
|
|
||||||
|
expect(eventBus.off).toHaveBeenCalledWith(
|
||||||
|
'pagerendered',
|
||||||
|
expect.any(Function)
|
||||||
|
)
|
||||||
|
expect(eventBus.off).toHaveBeenCalledWith('pagesinit', expect.any(Function))
|
||||||
|
expect(eventBus.off).toHaveBeenCalledWith(
|
||||||
|
'pagechanging',
|
||||||
|
expect.any(Function)
|
||||||
|
)
|
||||||
|
expect(resizeObserver.disconnect).toHaveBeenCalled()
|
||||||
|
expect(loadingTask.destroy).toHaveBeenCalled()
|
||||||
|
expect(viewer.cleanup).toHaveBeenCalled()
|
||||||
|
expect((component as any).pdfViewer).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips work when viewer is missing or has no pages', () => {
|
||||||
|
const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
|
||||||
|
const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
|
||||||
|
;(component as any).dispatchFindIfReady()
|
||||||
|
expect(dispatchSpy).not.toHaveBeenCalled()
|
||||||
|
;(component as any).applyViewerState()
|
||||||
|
;(component as any).applyScale()
|
||||||
|
|
||||||
|
const viewer = new PDFViewer({ eventBus: undefined })
|
||||||
|
viewer.pagesCount = 0
|
||||||
|
;(component as any).pdfViewer = viewer
|
||||||
|
viewer.currentScale = 5
|
||||||
|
;(component as any).applyScale()
|
||||||
|
expect(viewer.currentScale).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns early on src change in ngOnChanges', () => {
|
||||||
|
const loadSpy = jest.spyOn(component as any, 'loadDocument')
|
||||||
|
const initSpy = jest.spyOn(component as any, 'initViewer')
|
||||||
|
const scaleSpy = jest.spyOn(component as any, 'applyViewerState')
|
||||||
|
const resizeSpy = jest.spyOn(component as any, 'setupResizeObserver')
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
src: new SimpleChange(undefined, 'test.pdf', true),
|
||||||
|
zoomScale: new SimpleChange(
|
||||||
|
PdfZoomScale.PageWidth,
|
||||||
|
PdfZoomScale.PageFit,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(loadSpy).toHaveBeenCalled()
|
||||||
|
expect(resizeSpy).not.toHaveBeenCalled()
|
||||||
|
expect(initSpy).not.toHaveBeenCalled()
|
||||||
|
expect(scaleSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies viewer state after view init when already loaded', () => {
|
||||||
|
const applySpy = jest.spyOn(component as any, 'applyViewerState')
|
||||||
|
;(component as any).hasLoaded = true
|
||||||
|
;(component as any).pdf = { numPages: 1 }
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(applySpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips viewer state after view init when no pdf is available', () => {
|
||||||
|
const applySpy = jest.spyOn(component as any, 'applyViewerState')
|
||||||
|
;(component as any).hasLoaded = true
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(applySpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not reload when already loaded', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
|
||||||
|
const callCount = getDocumentSpy.mock.calls.length
|
||||||
|
await (component as any).loadDocument()
|
||||||
|
|
||||||
|
expect(getDocumentSpy).toHaveBeenCalledTimes(callCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('runs applyScale on resize observer notifications', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const applySpy = jest.spyOn(component as any, 'applyScale')
|
||||||
|
const resizeObserver = (component as any).resizeObserver as {
|
||||||
|
trigger: () => void
|
||||||
|
}
|
||||||
|
resizeObserver.trigger()
|
||||||
|
|
||||||
|
expect(applySpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips page work when no pages are available', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as PDFViewer
|
||||||
|
viewer.pagesCount = 0
|
||||||
|
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
|
||||||
|
|
||||||
|
component.page = undefined
|
||||||
|
;(component as any).lastViewerPage = 1
|
||||||
|
;(component as any).applyViewerState()
|
||||||
|
|
||||||
|
expect(applyScaleSpy).not.toHaveBeenCalled()
|
||||||
|
expect((component as any).lastViewerPage).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to a default zoom when input is invalid', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as PDFViewer
|
||||||
|
viewer.currentScale = 3
|
||||||
|
component.zoom = 'not-a-number' as PdfZoomLevel
|
||||||
|
;(component as any).applyScale()
|
||||||
|
|
||||||
|
expect(viewer.currentScale).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('re-initializes viewer on selectable or render mode changes', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const initSpy = jest.spyOn(component as any, 'initViewer')
|
||||||
|
component.selectable = false
|
||||||
|
component.renderMode = PdfRenderMode.Single
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
selectable: new SimpleChange(true, false, false),
|
||||||
|
renderMode: new SimpleChange(
|
||||||
|
PdfRenderMode.All,
|
||||||
|
PdfRenderMode.Single,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(initSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
Output,
|
||||||
|
SimpleChanges,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import {
|
||||||
|
getDocument,
|
||||||
|
GlobalWorkerOptions,
|
||||||
|
PDFDocumentLoadingTask,
|
||||||
|
PDFDocumentProxy,
|
||||||
|
} from 'pdfjs-dist/legacy/build/pdf.mjs'
|
||||||
|
import {
|
||||||
|
EventBus,
|
||||||
|
PDFFindController,
|
||||||
|
PDFLinkService,
|
||||||
|
PDFSinglePageViewer,
|
||||||
|
PDFViewer,
|
||||||
|
} from 'pdfjs-dist/web/pdf_viewer.mjs'
|
||||||
|
import {
|
||||||
|
PdfRenderMode,
|
||||||
|
PdfSource,
|
||||||
|
PdfZoomLevel,
|
||||||
|
PdfZoomScale,
|
||||||
|
PngxPdfDocumentProxy,
|
||||||
|
} from './pdf-viewer.types'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-pdf-viewer',
|
||||||
|
templateUrl: './pdf-viewer.component.html',
|
||||||
|
styleUrl: './pdf-viewer.component.scss',
|
||||||
|
})
|
||||||
|
export class PngxPdfViewerComponent
|
||||||
|
implements AfterViewInit, OnChanges, OnDestroy
|
||||||
|
{
|
||||||
|
@Input() src!: PdfSource
|
||||||
|
@Input() page?: number
|
||||||
|
@Output() pageChange = new EventEmitter<number>()
|
||||||
|
@Input() rotation?: number
|
||||||
|
@Input() renderMode: PdfRenderMode = PdfRenderMode.All
|
||||||
|
@Input() selectable = true
|
||||||
|
@Input() searchQuery = ''
|
||||||
|
@Input() zoom: PdfZoomLevel = PdfZoomLevel.One
|
||||||
|
@Input() zoomScale: PdfZoomScale = PdfZoomScale.PageWidth
|
||||||
|
|
||||||
|
@Output() afterLoadComplete = new EventEmitter<PngxPdfDocumentProxy>()
|
||||||
|
@Output() rendered = new EventEmitter<void>()
|
||||||
|
@Output() loadError = new EventEmitter<unknown>()
|
||||||
|
|
||||||
|
@ViewChild('container', { static: true })
|
||||||
|
private readonly container!: ElementRef<HTMLDivElement>
|
||||||
|
|
||||||
|
@ViewChild('viewer', { static: true })
|
||||||
|
private readonly viewer!: ElementRef<HTMLDivElement>
|
||||||
|
|
||||||
|
private hasLoaded = false
|
||||||
|
private loadingTask?: PDFDocumentLoadingTask
|
||||||
|
private resizeObserver?: ResizeObserver
|
||||||
|
private pdf?: PDFDocumentProxy
|
||||||
|
private pdfViewer?: PDFViewer | PDFSinglePageViewer
|
||||||
|
private hasRenderedPage = false
|
||||||
|
private lastFindQuery = ''
|
||||||
|
private lastViewerPage?: number
|
||||||
|
|
||||||
|
private readonly eventBus = new EventBus()
|
||||||
|
private readonly linkService = new PDFLinkService({ eventBus: this.eventBus })
|
||||||
|
private readonly findController = new PDFFindController({
|
||||||
|
eventBus: this.eventBus,
|
||||||
|
linkService: this.linkService,
|
||||||
|
updateMatchesCountOnProgress: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
private readonly onPageRendered = () => {
|
||||||
|
this.hasRenderedPage = true
|
||||||
|
this.dispatchFindIfReady()
|
||||||
|
this.rendered.emit()
|
||||||
|
}
|
||||||
|
private readonly onPagesInit = () => this.applyScale()
|
||||||
|
private readonly onPageChanging = (evt: { pageNumber: number }) => {
|
||||||
|
// Avoid [(page)] two-way binding re-triggers navigation
|
||||||
|
this.lastViewerPage = evt.pageNumber
|
||||||
|
this.pageChange.emit(evt.pageNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes['src']) {
|
||||||
|
this.hasLoaded = false
|
||||||
|
this.loadDocument()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes['zoomScale']) {
|
||||||
|
this.setupResizeObserver()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes['selectable'] || changes['renderMode']) {
|
||||||
|
this.initViewer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
changes['page'] ||
|
||||||
|
changes['zoom'] ||
|
||||||
|
changes['zoomScale'] ||
|
||||||
|
changes['rotation']
|
||||||
|
) {
|
||||||
|
this.applyViewerState()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes['searchQuery']) {
|
||||||
|
this.dispatchFindIfReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.setupResizeObserver()
|
||||||
|
this.initViewer()
|
||||||
|
if (!this.hasLoaded) {
|
||||||
|
this.loadDocument()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.pdf) {
|
||||||
|
this.applyViewerState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.eventBus.off('pagerendered', this.onPageRendered)
|
||||||
|
this.eventBus.off('pagesinit', this.onPagesInit)
|
||||||
|
this.eventBus.off('pagechanging', this.onPageChanging)
|
||||||
|
this.resizeObserver?.disconnect()
|
||||||
|
this.loadingTask?.destroy()
|
||||||
|
this.pdfViewer?.cleanup()
|
||||||
|
this.pdfViewer = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadDocument(): Promise<void> {
|
||||||
|
if (this.hasLoaded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasLoaded = true
|
||||||
|
this.hasRenderedPage = false
|
||||||
|
this.lastFindQuery = ''
|
||||||
|
this.loadingTask?.destroy()
|
||||||
|
|
||||||
|
GlobalWorkerOptions.workerSrc = '/assets/js/pdf.worker.min.mjs'
|
||||||
|
this.loadingTask = getDocument(this.src)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pdf = await this.loadingTask.promise
|
||||||
|
this.pdf = pdf
|
||||||
|
this.linkService.setDocument(pdf)
|
||||||
|
this.findController.onIsPageVisible = () => true
|
||||||
|
this.pdfViewer?.setDocument(pdf)
|
||||||
|
this.applyViewerState()
|
||||||
|
this.afterLoadComplete.emit(pdf)
|
||||||
|
} catch (err) {
|
||||||
|
this.loadError.emit(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupResizeObserver(): void {
|
||||||
|
this.resizeObserver?.disconnect()
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
this.applyScale()
|
||||||
|
})
|
||||||
|
this.resizeObserver.observe(this.container.nativeElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
private initViewer(): void {
|
||||||
|
this.viewer.nativeElement.innerHTML = ''
|
||||||
|
this.pdfViewer?.cleanup()
|
||||||
|
this.hasRenderedPage = false
|
||||||
|
this.lastFindQuery = ''
|
||||||
|
|
||||||
|
const textLayerMode = this.selectable === false ? 0 : 1
|
||||||
|
const options = {
|
||||||
|
container: this.container.nativeElement,
|
||||||
|
viewer: this.viewer.nativeElement,
|
||||||
|
eventBus: this.eventBus,
|
||||||
|
linkService: this.linkService,
|
||||||
|
findController: this.findController,
|
||||||
|
textLayerMode,
|
||||||
|
removePageBorders: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pdfViewer =
|
||||||
|
this.renderMode === PdfRenderMode.Single
|
||||||
|
? new PDFSinglePageViewer(options)
|
||||||
|
: new PDFViewer(options)
|
||||||
|
this.linkService.setViewer(this.pdfViewer)
|
||||||
|
|
||||||
|
this.eventBus.off('pagerendered', this.onPageRendered)
|
||||||
|
this.eventBus.off('pagesinit', this.onPagesInit)
|
||||||
|
this.eventBus.off('pagechanging', this.onPageChanging)
|
||||||
|
this.eventBus.on('pagerendered', this.onPageRendered)
|
||||||
|
this.eventBus.on('pagesinit', this.onPagesInit)
|
||||||
|
this.eventBus.on('pagechanging', this.onPageChanging)
|
||||||
|
|
||||||
|
if (this.pdf) {
|
||||||
|
this.pdfViewer.setDocument(this.pdf)
|
||||||
|
this.applyViewerState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyViewerState(): void {
|
||||||
|
if (!this.pdfViewer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const hasPages = this.pdfViewer.pagesCount > 0
|
||||||
|
if (typeof this.rotation === 'number' && hasPages) {
|
||||||
|
this.pdfViewer.pagesRotation = this.rotation
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof this.page === 'number' &&
|
||||||
|
hasPages &&
|
||||||
|
this.page !== this.lastViewerPage
|
||||||
|
) {
|
||||||
|
this.pdfViewer.currentPageNumber = this.page
|
||||||
|
}
|
||||||
|
if (this.page === this.lastViewerPage) {
|
||||||
|
this.lastViewerPage = undefined
|
||||||
|
}
|
||||||
|
if (hasPages) {
|
||||||
|
this.applyScale()
|
||||||
|
}
|
||||||
|
this.dispatchFindIfReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyScale(): void {
|
||||||
|
if (!this.pdfViewer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.pdfViewer.pagesCount === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const zoomFactor = Number(this.zoom) || 1
|
||||||
|
this.pdfViewer.currentScaleValue = this.zoomScale
|
||||||
|
if (zoomFactor !== 1) {
|
||||||
|
this.pdfViewer.currentScale = this.pdfViewer.currentScale * zoomFactor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatchFindIfReady(): void {
|
||||||
|
if (!this.hasRenderedPage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const query = this.searchQuery.trim()
|
||||||
|
if (query === this.lastFindQuery) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.lastFindQuery = query
|
||||||
|
this.eventBus.dispatch('find', {
|
||||||
|
query,
|
||||||
|
caseSensitive: false,
|
||||||
|
highlightAll: query.length > 0,
|
||||||
|
phraseSearch: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export type PngxPdfDocumentProxy = {
|
||||||
|
numPages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PdfSource = string | { url: string; password?: string }
|
||||||
|
|
||||||
|
export enum PdfRenderMode {
|
||||||
|
Single = 'single',
|
||||||
|
All = 'all',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PdfZoomScale {
|
||||||
|
PageFit = 'page-fit',
|
||||||
|
PageWidth = 'page-width',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PdfZoomLevel {
|
||||||
|
Quarter = '.25',
|
||||||
|
Half = '.5',
|
||||||
|
ThreeQuarters = '.75',
|
||||||
|
One = '1',
|
||||||
|
OneAndHalf = '1.5',
|
||||||
|
Two = '2',
|
||||||
|
Three = '3',
|
||||||
|
}
|
||||||
@@ -23,14 +23,12 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (!requiresPassword) {
|
@if (!requiresPassword) {
|
||||||
<pdf-viewer
|
<pngx-pdf-viewer
|
||||||
[src]="previewUrl"
|
[src]="previewUrl"
|
||||||
[original-size]="false"
|
[renderMode]="PdfRenderMode.All"
|
||||||
[show-borders]="false"
|
[searchQuery]="documentService.searchQuery"
|
||||||
[show-all]="true"
|
(loadError)="onError($event)">
|
||||||
(text-layer-rendered)="onPageRendered()"
|
</pngx-pdf-viewer>
|
||||||
(error)="onError($event)" #pdfViewer>
|
|
||||||
</pdf-viewer>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { of, throwError } from 'rxjs'
|
|||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
|
||||||
import { PreviewPopupComponent } from './preview-popup.component'
|
import { PreviewPopupComponent } from './preview-popup.component'
|
||||||
|
|
||||||
const doc = {
|
const doc = {
|
||||||
@@ -78,7 +79,7 @@ describe('PreviewPopupComponent', () => {
|
|||||||
component.popover.open()
|
component.popover.open()
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(fixture.debugElement.query(By.css('object'))).toBeNull()
|
expect(fixture.debugElement.query(By.css('object'))).toBeNull()
|
||||||
expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
|
expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show lock icon on password error', () => {
|
it('should show lock icon on password error', () => {
|
||||||
@@ -159,23 +160,15 @@ describe('PreviewPopupComponent', () => {
|
|||||||
expect(component.popover.isOpen()).toBeFalsy()
|
expect(component.popover.isOpen()).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should dispatch find event on viewer loaded if searchQuery set', () => {
|
it('should pass searchQuery to viewer', () => {
|
||||||
documentService.searchQuery = 'test'
|
documentService.searchQuery = 'test'
|
||||||
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
||||||
component.popover.open()
|
component.popover.open()
|
||||||
jest.advanceTimersByTime(1000)
|
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
// normally setup by pdf-viewer
|
const viewer = fixture.debugElement.query(
|
||||||
jest.replaceProperty(component.pdfViewer, 'eventBus', {
|
By.directive(PngxPdfViewerComponent)
|
||||||
dispatch: jest.fn(),
|
)
|
||||||
} as any)
|
expect(viewer).not.toBeNull()
|
||||||
const dispatchSpy = jest.spyOn(component.pdfViewer.eventBus, 'dispatch')
|
expect(viewer.componentInstance.searchQuery).toBe('test')
|
||||||
component.onPageRendered()
|
|
||||||
expect(dispatchSpy).toHaveBeenCalledWith('find', {
|
|
||||||
query: 'test',
|
|
||||||
caseSensitive: false,
|
|
||||||
highlightAll: true,
|
|
||||||
phraseSearch: true,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Component, inject, Input, OnDestroy, ViewChild } from '@angular/core'
|
import { Component, inject, Input, OnDestroy, ViewChild } from '@angular/core'
|
||||||
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { PdfViewerComponent, PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { first, Subject, takeUntil } from 'rxjs'
|
import { first, Subject, takeUntil } from 'rxjs'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
@@ -10,6 +9,8 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
|||||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
|
||||||
|
import { PdfRenderMode } from '../pdf-viewer/pdf-viewer.types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-preview-popup',
|
selector: 'pngx-preview-popup',
|
||||||
@@ -18,14 +19,15 @@ import { SettingsService } from 'src/app/services/settings.service'
|
|||||||
imports: [
|
imports: [
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
DocumentTitlePipe,
|
DocumentTitlePipe,
|
||||||
PdfViewerModule,
|
PngxPdfViewerComponent,
|
||||||
SafeUrlPipe,
|
SafeUrlPipe,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PreviewPopupComponent implements OnDestroy {
|
export class PreviewPopupComponent implements OnDestroy {
|
||||||
|
PdfRenderMode = PdfRenderMode
|
||||||
private settingsService = inject(SettingsService)
|
private settingsService = inject(SettingsService)
|
||||||
private documentService = inject(DocumentService)
|
public readonly documentService = inject(DocumentService)
|
||||||
private http = inject(HttpClient)
|
private http = inject(HttpClient)
|
||||||
|
|
||||||
private _document: Document
|
private _document: Document
|
||||||
@@ -61,8 +63,6 @@ export class PreviewPopupComponent implements OnDestroy {
|
|||||||
|
|
||||||
@ViewChild('popover') popover: NgbPopover
|
@ViewChild('popover') popover: NgbPopover
|
||||||
|
|
||||||
@ViewChild('pdfViewer') pdfViewer: PdfViewerComponent
|
|
||||||
|
|
||||||
mouseOnPreview: boolean = false
|
mouseOnPreview: boolean = false
|
||||||
|
|
||||||
popoverClass: string = 'shadow popover-preview'
|
popoverClass: string = 'shadow popover-preview'
|
||||||
@@ -114,18 +114,6 @@ export class PreviewPopupComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageRendered() {
|
|
||||||
// Only triggered by the pngx pdf viewer
|
|
||||||
if (this.documentService.searchQuery) {
|
|
||||||
this.pdfViewer.eventBus.dispatch('find', {
|
|
||||||
query: this.documentService.searchQuery,
|
|
||||||
caseSensitive: false,
|
|
||||||
highlightAll: true,
|
|
||||||
phraseSearch: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mouseEnterPreview() {
|
mouseEnterPreview() {
|
||||||
this.mouseOnPreview = true
|
this.mouseOnPreview = true
|
||||||
if (!this.popover.isOpen()) {
|
if (!this.popover.isOpen()) {
|
||||||
|
|||||||
@@ -456,17 +456,15 @@
|
|||||||
@case (ContentRenderType.PDF) {
|
@case (ContentRenderType.PDF) {
|
||||||
@if (!useNativePdfViewer) {
|
@if (!useNativePdfViewer) {
|
||||||
<div class="preview-sticky pdf-viewer-container">
|
<div class="preview-sticky pdf-viewer-container">
|
||||||
<pdf-viewer
|
<pngx-pdf-viewer
|
||||||
[src]="{ url: previewUrl, password: password }"
|
[src]="{ url: previewUrl, password: password }"
|
||||||
[original-size]="false"
|
[renderMode]="PdfRenderMode.All"
|
||||||
[show-borders]="true"
|
|
||||||
[show-all]="true"
|
|
||||||
[(page)]="previewCurrentPage"
|
[(page)]="previewCurrentPage"
|
||||||
[zoom-scale]="previewZoomScale"
|
[zoomScale]="previewZoomScale"
|
||||||
[zoom]="previewZoomSetting"
|
[zoom]="previewZoomSetting"
|
||||||
(error)="onError($event)"
|
(loadError)="onError($event)"
|
||||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
(afterLoadComplete)="pdfPreviewLoaded($event)">
|
||||||
</pdf-viewer>
|
</pngx-pdf-viewer>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||||
|
|||||||
@@ -5,20 +5,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pdf-viewer-container {
|
.pdf-viewer-container {
|
||||||
padding-top: 10px;
|
padding: 8px;
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
|
|
||||||
pdf-viewer {
|
pngx-pdf-viewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .ng2-pdf-viewer-container .page {
|
|
||||||
--page-margin: 0 auto 10px;
|
|
||||||
--page-border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group .dropdown-toggle-split {
|
.btn-group .dropdown-toggle-split {
|
||||||
border-top-right-radius: inherit;
|
border-top-right-radius: inherit;
|
||||||
border-bottom-right-radius: inherit;
|
border-bottom-right-radius: inherit;
|
||||||
|
|||||||
@@ -69,8 +69,11 @@ import { environment } from 'src/environments/environment'
|
|||||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
||||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||||
|
import {
|
||||||
|
PdfZoomLevel,
|
||||||
|
PdfZoomScale,
|
||||||
|
} from '../common/pdf-viewer/pdf-viewer.types'
|
||||||
import { DocumentDetailComponent } from './document-detail.component'
|
import { DocumentDetailComponent } from './document-detail.component'
|
||||||
import { ZoomSetting } from './zoom-setting'
|
|
||||||
|
|
||||||
const doc: Document = {
|
const doc: Document = {
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -860,7 +863,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
it('should support zoom controls', () => {
|
it('should support zoom controls', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.setZoom(ZoomSetting.One) // from select
|
component.setZoom(PdfZoomLevel.One) // from select
|
||||||
expect(component.previewZoomSetting).toEqual('1')
|
expect(component.previewZoomSetting).toEqual('1')
|
||||||
component.increaseZoom()
|
component.increaseZoom()
|
||||||
expect(component.previewZoomSetting).toEqual('1.5')
|
expect(component.previewZoomSetting).toEqual('1.5')
|
||||||
@@ -868,18 +871,18 @@ describe('DocumentDetailComponent', () => {
|
|||||||
expect(component.previewZoomSetting).toEqual('2')
|
expect(component.previewZoomSetting).toEqual('2')
|
||||||
component.decreaseZoom()
|
component.decreaseZoom()
|
||||||
expect(component.previewZoomSetting).toEqual('1.5')
|
expect(component.previewZoomSetting).toEqual('1.5')
|
||||||
component.setZoom(ZoomSetting.One) // from select
|
component.setZoom(PdfZoomLevel.One) // from select
|
||||||
component.decreaseZoom()
|
component.decreaseZoom()
|
||||||
expect(component.previewZoomSetting).toEqual('.75')
|
expect(component.previewZoomSetting).toEqual('.75')
|
||||||
|
|
||||||
component.setZoom(ZoomSetting.PageFit) // from select
|
component.setZoom(PdfZoomScale.PageFit) // from select
|
||||||
expect(component.previewZoomScale).toEqual('page-fit')
|
expect(component.previewZoomScale).toEqual('page-fit')
|
||||||
expect(component.previewZoomSetting).toEqual('1')
|
expect(component.previewZoomSetting).toEqual('1')
|
||||||
component.increaseZoom()
|
component.increaseZoom()
|
||||||
expect(component.previewZoomSetting).toEqual('1.5')
|
expect(component.previewZoomSetting).toEqual('1.5')
|
||||||
expect(component.previewZoomScale).toEqual('page-width')
|
expect(component.previewZoomScale).toEqual('page-width')
|
||||||
|
|
||||||
component.setZoom(ZoomSetting.PageFit) // from select
|
component.setZoom(PdfZoomScale.PageFit) // from select
|
||||||
expect(component.previewZoomScale).toEqual('page-fit')
|
expect(component.previewZoomScale).toEqual('page-fit')
|
||||||
expect(component.previewZoomSetting).toEqual('1')
|
expect(component.previewZoomSetting).toEqual('1')
|
||||||
component.decreaseZoom()
|
component.decreaseZoom()
|
||||||
@@ -889,10 +892,10 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
it('should select correct zoom setting in dropdown', () => {
|
it('should select correct zoom setting in dropdown', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.setZoom(ZoomSetting.PageFit)
|
component.setZoom(PdfZoomScale.PageFit)
|
||||||
expect(component.currentZoom).toEqual(ZoomSetting.PageFit)
|
expect(component.currentZoom).toEqual(PdfZoomScale.PageFit)
|
||||||
component.setZoom(ZoomSetting.Quarter)
|
component.setZoom(PdfZoomLevel.Quarter)
|
||||||
expect(component.currentZoom).toEqual(ZoomSetting.Quarter)
|
expect(component.currentZoom).toEqual(PdfZoomLevel.Quarter)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support updating notes dynamically', () => {
|
it('should support updating notes dynamically', () => {
|
||||||
@@ -1017,7 +1020,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
||||||
expect(component.useNativePdfViewer).toBeFalsy()
|
expect(component.useNativePdfViewer).toBeFalsy()
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
|
expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display native pdf viewer if enabled', () => {
|
it('should display native pdf viewer if enabled', () => {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
||||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||||
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
|
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
|
||||||
@@ -108,13 +107,19 @@ import { UrlComponent } from '../common/input/url/url.component'
|
|||||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||||
import { PdfEditorEditMode } from '../common/pdf-editor/pdf-editor-edit-mode'
|
import { PdfEditorEditMode } from '../common/pdf-editor/pdf-editor-edit-mode'
|
||||||
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
|
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
|
||||||
|
import { PngxPdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component'
|
||||||
|
import {
|
||||||
|
PdfRenderMode,
|
||||||
|
PdfZoomLevel,
|
||||||
|
PdfZoomScale,
|
||||||
|
PngxPdfDocumentProxy,
|
||||||
|
} from '../common/pdf-viewer/pdf-viewer.types'
|
||||||
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
||||||
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
|
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
|
||||||
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
||||||
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
||||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
|
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
|
||||||
import { ZoomSetting } from './zoom-setting'
|
|
||||||
|
|
||||||
enum DocumentDetailNavIDs {
|
enum DocumentDetailNavIDs {
|
||||||
Details = 1,
|
Details = 1,
|
||||||
@@ -168,16 +173,17 @@ enum ContentRenderType {
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
PdfViewerModule,
|
|
||||||
TextAreaComponent,
|
TextAreaComponent,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
PngxPdfViewerComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentDetailComponent
|
export class DocumentDetailComponent
|
||||||
extends ComponentWithPermissions
|
extends ComponentWithPermissions
|
||||||
implements OnInit, OnDestroy, DirtyComponent
|
implements OnInit, OnDestroy, DirtyComponent
|
||||||
{
|
{
|
||||||
private documentsService = inject(DocumentService)
|
PdfRenderMode = PdfRenderMode
|
||||||
|
documentsService = inject(DocumentService)
|
||||||
private route = inject(ActivatedRoute)
|
private route = inject(ActivatedRoute)
|
||||||
private tagService = inject(TagService)
|
private tagService = inject(TagService)
|
||||||
private correspondentService = inject(CorrespondentService)
|
private correspondentService = inject(CorrespondentService)
|
||||||
@@ -246,8 +252,8 @@ export class DocumentDetailComponent
|
|||||||
|
|
||||||
previewCurrentPage: number = 1
|
previewCurrentPage: number = 1
|
||||||
previewNumPages: number
|
previewNumPages: number
|
||||||
previewZoomSetting: ZoomSetting = ZoomSetting.One
|
previewZoomSetting: PdfZoomLevel = PdfZoomLevel.One
|
||||||
previewZoomScale: ZoomSetting = ZoomSetting.PageWidth
|
previewZoomScale: PdfZoomScale = PdfZoomScale.PageWidth
|
||||||
|
|
||||||
store: BehaviorSubject<any>
|
store: BehaviorSubject<any>
|
||||||
isDirty$: Observable<boolean>
|
isDirty$: Observable<boolean>
|
||||||
@@ -503,7 +509,9 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.setZoom(this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING))
|
this.setZoom(
|
||||||
|
this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING) as PdfZoomScale
|
||||||
|
)
|
||||||
this.documentForm.valueChanges
|
this.documentForm.valueChanges
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((values) => {
|
.subscribe((values) => {
|
||||||
@@ -1204,7 +1212,7 @@ export class DocumentDetailComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
|
pdfPreviewLoaded(pdf: PngxPdfDocumentProxy) {
|
||||||
this.previewNumPages = pdf.numPages
|
this.previewNumPages = pdf.numPages
|
||||||
if (this.password) this.requiresPassword = false
|
if (this.password) this.requiresPassword = false
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1225,31 +1233,33 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setZoom(setting: ZoomSetting) {
|
setZoom(setting: PdfZoomScale | PdfZoomLevel) {
|
||||||
if (ZoomSetting.PageFit === setting || ZoomSetting.PageWidth === setting) {
|
if (
|
||||||
|
setting === PdfZoomScale.PageFit ||
|
||||||
|
setting === PdfZoomScale.PageWidth
|
||||||
|
) {
|
||||||
this.previewZoomScale = setting
|
this.previewZoomScale = setting
|
||||||
this.previewZoomSetting = ZoomSetting.One
|
this.previewZoomSetting = PdfZoomLevel.One
|
||||||
} else {
|
return
|
||||||
this.previewZoomSetting = setting
|
|
||||||
this.previewZoomScale = ZoomSetting.PageWidth
|
|
||||||
}
|
}
|
||||||
|
this.previewZoomSetting = setting
|
||||||
|
this.previewZoomScale = PdfZoomScale.PageWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
get zoomSettings() {
|
get zoomSettings() {
|
||||||
return Object.values(ZoomSetting).filter(
|
return [PdfZoomScale.PageFit, ...Object.values(PdfZoomLevel)]
|
||||||
(setting) => setting !== ZoomSetting.PageWidth
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentZoom() {
|
get currentZoom() {
|
||||||
if (this.previewZoomScale === ZoomSetting.PageFit) {
|
if (this.previewZoomScale === PdfZoomScale.PageFit) {
|
||||||
return ZoomSetting.PageFit
|
return PdfZoomScale.PageFit
|
||||||
} else return this.previewZoomSetting
|
}
|
||||||
|
return this.previewZoomSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
getZoomSettingTitle(setting: ZoomSetting): string {
|
getZoomSettingTitle(setting: PdfZoomScale | PdfZoomLevel): string {
|
||||||
switch (setting) {
|
switch (setting) {
|
||||||
case ZoomSetting.PageFit:
|
case PdfZoomScale.PageFit:
|
||||||
return $localize`Page Fit`
|
return $localize`Page Fit`
|
||||||
default:
|
default:
|
||||||
return `${parseFloat(setting) * 100}%`
|
return `${parseFloat(setting) * 100}%`
|
||||||
@@ -1257,25 +1267,24 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
increaseZoom(): void {
|
increaseZoom(): void {
|
||||||
let currentIndex = Object.values(ZoomSetting).indexOf(
|
const zoomLevels = Object.values(PdfZoomLevel)
|
||||||
this.previewZoomSetting
|
let currentIndex = zoomLevels.indexOf(this.previewZoomSetting)
|
||||||
)
|
if (this.previewZoomScale === PdfZoomScale.PageFit) {
|
||||||
if (this.previewZoomScale === ZoomSetting.PageFit) currentIndex = 5
|
currentIndex = zoomLevels.indexOf(PdfZoomLevel.One)
|
||||||
this.previewZoomScale = ZoomSetting.PageWidth
|
}
|
||||||
|
this.previewZoomScale = PdfZoomScale.PageWidth
|
||||||
this.previewZoomSetting =
|
this.previewZoomSetting =
|
||||||
Object.values(ZoomSetting)[
|
zoomLevels[Math.min(zoomLevels.length - 1, currentIndex + 1)]
|
||||||
Math.min(Object.values(ZoomSetting).length - 1, currentIndex + 1)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decreaseZoom(): void {
|
decreaseZoom(): void {
|
||||||
let currentIndex = Object.values(ZoomSetting).indexOf(
|
const zoomLevels = Object.values(PdfZoomLevel)
|
||||||
this.previewZoomSetting
|
let currentIndex = zoomLevels.indexOf(this.previewZoomSetting)
|
||||||
)
|
if (this.previewZoomScale === PdfZoomScale.PageFit) {
|
||||||
if (this.previewZoomScale === ZoomSetting.PageFit) currentIndex = 4
|
currentIndex = zoomLevels.indexOf(PdfZoomLevel.ThreeQuarters)
|
||||||
this.previewZoomScale = ZoomSetting.PageWidth
|
}
|
||||||
this.previewZoomSetting =
|
this.previewZoomScale = PdfZoomScale.PageWidth
|
||||||
Object.values(ZoomSetting)[Math.max(2, currentIndex - 1)]
|
this.previewZoomSetting = zoomLevels[Math.max(0, currentIndex - 1)]
|
||||||
}
|
}
|
||||||
|
|
||||||
get showPermissions(): boolean {
|
get showPermissions(): boolean {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
export enum ZoomSetting {
|
|
||||||
PageFit = 'page-fit',
|
|
||||||
PageWidth = 'page-width',
|
|
||||||
Quarter = '.25',
|
|
||||||
Half = '.5',
|
|
||||||
ThreeQuarters = '.75',
|
|
||||||
One = '1',
|
|
||||||
OneAndHalf = '1.5',
|
|
||||||
Two = '2',
|
|
||||||
Three = '3',
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
|
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
|
||||||
import { ZoomSetting } from '../components/document-detail/zoom-setting'
|
import { PdfZoomScale } from '../components/common/pdf-viewer/pdf-viewer.types'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
|
|
||||||
export interface UiSettings {
|
export interface UiSettings {
|
||||||
@@ -310,7 +310,7 @@ export const SETTINGS: UiSetting[] = [
|
|||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: ZoomSetting.PageWidth,
|
default: PdfZoomScale.PageWidth,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.AI_ENABLED,
|
key: SETTINGS_KEYS.AI_ENABLED,
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
NgbModule,
|
NgbModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import {
|
import {
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
airplane,
|
airplane,
|
||||||
@@ -371,7 +370,6 @@ bootstrapApplication(AppComponent, {
|
|||||||
NgbModule,
|
NgbModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
PdfViewerModule,
|
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
ColorSliderModule,
|
ColorSliderModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
|
|||||||
24
src-ui/src/test/mocks/pdfjs-legacy-build-pdf.ts
Normal file
24
src-ui/src/test/mocks/pdfjs-legacy-build-pdf.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export class PDFDocumentProxy {
|
||||||
|
numPages = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PDFDocumentLoadingTask {
|
||||||
|
promise: Promise<PDFDocumentProxy>
|
||||||
|
destroyed = false
|
||||||
|
|
||||||
|
constructor(promise: Promise<PDFDocumentProxy>) {
|
||||||
|
this.promise = promise
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.destroyed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalWorkerOptions = {
|
||||||
|
workerSrc: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDocument = (_src: unknown): PDFDocumentLoadingTask => {
|
||||||
|
return new PDFDocumentLoadingTask(Promise.resolve(new PDFDocumentProxy()))
|
||||||
|
}
|
||||||
79
src-ui/src/test/mocks/pdfjs-web-pdf_viewer.ts
Normal file
79
src-ui/src/test/mocks/pdfjs-web-pdf_viewer.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
type EventHandler = (event?: unknown) => void
|
||||||
|
|
||||||
|
export class EventBus {
|
||||||
|
private readonly listeners = new Map<string, Set<EventHandler>>()
|
||||||
|
|
||||||
|
on(eventName: string, listener: EventHandler): void {
|
||||||
|
let listeners = this.listeners.get(eventName)
|
||||||
|
if (!listeners) {
|
||||||
|
listeners = new Set()
|
||||||
|
this.listeners.set(eventName, listeners)
|
||||||
|
}
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
off(eventName: string, listener: EventHandler): void {
|
||||||
|
this.listeners.get(eventName)?.delete(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(eventName: string, event?: unknown): void {
|
||||||
|
this.listeners.get(eventName)?.forEach((listener) => listener(event))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PDFFindController {
|
||||||
|
onIsPageVisible?: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PDFLinkService {
|
||||||
|
private document?: unknown
|
||||||
|
private viewer?: unknown
|
||||||
|
|
||||||
|
setDocument(document: unknown): void {
|
||||||
|
this.document = document
|
||||||
|
}
|
||||||
|
|
||||||
|
setViewer(viewer: unknown): void {
|
||||||
|
this.viewer = viewer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseViewer {
|
||||||
|
pagesCount = 0
|
||||||
|
currentScale = 1
|
||||||
|
currentScaleValue: string | number = 1
|
||||||
|
pagesRotation = 0
|
||||||
|
readonly options: Record<string, unknown>
|
||||||
|
|
||||||
|
private readonly eventBus?: EventBus
|
||||||
|
private _currentPageNumber = 1
|
||||||
|
|
||||||
|
constructor(options: { eventBus?: EventBus }) {
|
||||||
|
this.options = options
|
||||||
|
this.eventBus = options.eventBus
|
||||||
|
}
|
||||||
|
|
||||||
|
setDocument(document: { numPages?: number } | null | undefined): void {
|
||||||
|
this.pagesCount = document?.numPages ?? 1
|
||||||
|
this.eventBus?.dispatch('pagesinit', {})
|
||||||
|
this.eventBus?.dispatch('pagerendered', {
|
||||||
|
pageNumber: this._currentPageNumber,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup(): void {
|
||||||
|
this.pagesCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentPageNumber(): number {
|
||||||
|
return this._currentPageNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
set currentPageNumber(value: number) {
|
||||||
|
this._currentPageNumber = value
|
||||||
|
this.eventBus?.dispatch('pagechanging', { pageNumber: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PDFViewer extends BaseViewer {}
|
||||||
|
export class PDFSinglePageViewer extends BaseViewer {}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.spec.ts",
|
"src/**/*.spec.ts",
|
||||||
"src/**/*.d.ts"
|
"src/**/*.d.ts",
|
||||||
|
"src/test/**/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user