pdfjs
This commit is contained in:
Generated
+284
@@ -11,6 +11,7 @@
|
|||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"pdfjs-dist": "^5.4.394",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.9.5"
|
"react-router-dom": "^7.9.5"
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
||||||
"@typescript-eslint/parser": "^8.18.1",
|
"@typescript-eslint/parser": "^8.18.1",
|
||||||
"babel-loader": "^10.0.0",
|
"babel-loader": "^10.0.0",
|
||||||
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
@@ -2307,6 +2309,191 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/canvas": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-FGjyUBoF0sl1EenSiE4UV2WYu76q6F9GSYedq5EiOCOyGYoQ/Owulcv6rd7v/tWOpljDDtefXXIaOCJrVKem4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"workspaces": [
|
||||||
|
"e2e/*"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas-android-arm64": "0.1.82",
|
||||||
|
"@napi-rs/canvas-darwin-arm64": "0.1.82",
|
||||||
|
"@napi-rs/canvas-darwin-x64": "0.1.82",
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.82",
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": "0.1.82",
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": "0.1.82",
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.82",
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": "0.1.82",
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": "0.1.82",
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": "0.1.82"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-bvZhN0iI54ouaQOrgJV96H2q7J3ZoufnHf4E1fUaERwW29Rz4rgicohnAg4venwBJZYjGl5Yl3CGmlAl1LZowQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-InuBHKCyuFqhNwNr4gpqazo5Xp6ltKflqOLiROn4hqAS8u21xAHyYCJRgHwd+a5NKmutFTaRWeUIT/vxWbU/iw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-aQGV5Ynn96onSXcuvYb2y7TRXD/t4CL2EGmnGqvLyeJX1JLSNisKQlWN/1bPDDXymZYSdUqbXehj5qzBlOx+RQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-YIUpmHWeHGGRhWitT1KJkgj/JPXPfc9ox8oUoyaGPxolLGPp5AxJkq8wIg8CdFGtutget968dtwmx71m8o3h5g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-AwLzwLBgmvk7kWeUgItOUor/QyG31xqtD26w1tLpf4yE0hiXTGp23yc669aawjB6FzgIkjh1NKaNS52B7/qEBQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-moZWuqepAwWBffdF4JDadt8TgBD02iMhG6I1FHZf8xO20AsIp9rB+p0B8Zma2h2vAF/YMjeFCDmW5un6+zZz9g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-w9++2df2kG9eC9LWYIHIlMLuhIrKGQYfUxs97CwgxYjITeFakIRazI9LYWgVzEc98QZ9x9GQvlicFsrROV59MQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-lZulOPwrRi6hEg/17CaqdwWEUfOlIJuhXxincx1aVzsVOCmyHf+xFq4i6liJl1P+x2v6Iz2Z/H5zHvXJCC7Bwg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-Be9Wf5RTv1w6GXlTph55K3PH3vsAh1Ax4T1FQY1UYM0QfD0yrwGdnJ8/fhqw7dEgMjd59zIbjJQC8C3msbGn5g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||||
|
"version": "0.1.82",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.82.tgz",
|
||||||
|
"integrity": "sha512-LN/i8VrvxTDmEEK1c10z2cdOTkWT76LlTGtyZe5Kr1sqoSomKeExAjbilnu1+oee5lZUgS5yfZ2LNlVhCeARuw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -4524,6 +4711,43 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/copy-webpack-plugin": {
|
||||||
|
"version": "13.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz",
|
||||||
|
"integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"glob-parent": "^6.0.1",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"schema-utils": "^4.2.0",
|
||||||
|
"serialize-javascript": "^6.0.2",
|
||||||
|
"tinyglobby": "^0.2.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/webpack"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"webpack": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/copy-webpack-plugin/node_modules/glob-parent": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.46.0",
|
"version": "3.46.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
|
||||||
@@ -8159,6 +8383,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pdfjs-dist": {
|
||||||
|
"version": "5.4.394",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.394.tgz",
|
||||||
|
"integrity": "sha512-9ariAYGqUJzx+V/1W4jHyiyCep6IZALmDzoaTLZ6VNu8q9LWi1/ukhzHgE2Xsx96AZi0mbZuK4/ttIbqSbLypg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.16.0 || >=22.3.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas": "^0.1.81"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -9905,6 +10141,54 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyglobby": {
|
||||||
|
"version": "0.2.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fdir": "^6.5.0",
|
||||||
|
"picomatch": "^4.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby/node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"pdfjs-dist": "^5.4.394",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.9.5"
|
"react-router-dom": "^7.9.5"
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
||||||
"@typescript-eslint/parser": "^8.18.1",
|
"@typescript-eslint/parser": "^8.18.1",
|
||||||
"babel-loader": "^10.0.0",
|
"babel-loader": "^10.0.0",
|
||||||
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ interface UseDocumentPreviewArgs {
|
|||||||
interface UseDocumentPreviewResult {
|
interface UseDocumentPreviewResult {
|
||||||
previewEntries: Map<DocumentId, PreviewEntry>;
|
previewEntries: Map<DocumentId, PreviewEntry>;
|
||||||
previewDocuments: Map<DocumentId, DocumentLike>;
|
previewDocuments: Map<DocumentId, DocumentLike>;
|
||||||
ensurePreviewUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise<PreviewEntry | null>;
|
ensureDownloadUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise<PreviewEntry | null>;
|
||||||
ensurePreviewData: (documentId: DocumentId) => Promise<DocumentLike | null>;
|
ensurePreviewData: (documentId: DocumentId) => Promise<DocumentLike | null>;
|
||||||
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
|
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
|
||||||
closeDocumentPreview: (folderId?: FolderId) => void;
|
closeDocumentPreview: (folderId?: FolderId) => void;
|
||||||
@@ -143,7 +143,7 @@ const useDocumentPreview = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const ensurePreviewUrl = useCallback(
|
const ensureDownloadUrl = useCallback(
|
||||||
async (documentId: DocumentId, { force = false }: { force?: boolean } = {}): Promise<PreviewEntry | null> => {
|
async (documentId: DocumentId, { force = false }: { force?: boolean } = {}): Promise<PreviewEntry | null> => {
|
||||||
if (!documentId) return null;
|
if (!documentId) return null;
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ const useDocumentPreview = ({
|
|||||||
fallbackFolderId === 'root' ? '/documents' : `/documents/folder/${fallbackFolderId}`;
|
fallbackFolderId === 'root' ? '/documents' : `/documents/folder/${fallbackFolderId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ensurePreviewUrl(documentId, { force: false });
|
await ensureDownloadUrl(documentId, { force: false });
|
||||||
setActivePreviewId(documentId);
|
setActivePreviewId(documentId);
|
||||||
return doc;
|
return doc;
|
||||||
},
|
},
|
||||||
@@ -239,7 +239,7 @@ const useDocumentPreview = ({
|
|||||||
searchResults,
|
searchResults,
|
||||||
documents,
|
documents,
|
||||||
assetManager,
|
assetManager,
|
||||||
ensurePreviewUrl,
|
ensureDownloadUrl,
|
||||||
setActivePreviewId,
|
setActivePreviewId,
|
||||||
api,
|
api,
|
||||||
cachePreviewDocument,
|
cachePreviewDocument,
|
||||||
@@ -336,7 +336,7 @@ const useDocumentPreview = ({
|
|||||||
return {
|
return {
|
||||||
previewEntries,
|
previewEntries,
|
||||||
previewDocuments,
|
previewDocuments,
|
||||||
ensurePreviewUrl,
|
ensureDownloadUrl,
|
||||||
ensurePreviewData,
|
ensurePreviewData,
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
closeDocumentPreview,
|
closeDocumentPreview,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const DesktopPreviewCard = ({
|
|||||||
}: DesktopPreviewCardProps): JSX.Element => {
|
}: DesktopPreviewCardProps): JSX.Element => {
|
||||||
const navigator = useAssetNavigator({
|
const navigator = useAssetNavigator({
|
||||||
document: doc,
|
document: doc,
|
||||||
assetType: 'preview',
|
assetType: 'thumbnail',
|
||||||
ensureAssetUrl: shouldLoad ? ensureAssetUrl : null,
|
ensureAssetUrl: shouldLoad ? ensureAssetUrl : null,
|
||||||
getAsset: getDocumentAsset,
|
getAsset: getDocumentAsset,
|
||||||
prefetch,
|
prefetch,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import React, {
|
|||||||
useSyncExternalStore,
|
useSyncExternalStore,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { resolveDocumentAssetUrl } from '../asset_manager';
|
import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||||
|
import type { EnsureAssetUrl, GetAsset } from '../asset_manager';
|
||||||
import { formatTransform } from './math';
|
import { formatTransform } from './math';
|
||||||
import useDocumentDrag from './useDocumentDrag';
|
import useDocumentDrag from './useDocumentDrag';
|
||||||
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
||||||
@@ -31,6 +32,8 @@ import '../styles/workspace/workspace-cards.css';
|
|||||||
type Identifier = string | number;
|
type Identifier = string | number;
|
||||||
|
|
||||||
type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null;
|
type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null;
|
||||||
|
type PreviewEntryLike = { url?: string | null; contentType?: string | null };
|
||||||
|
type OverlaySource = { url: string; alt?: string | null; contentType?: string | null };
|
||||||
|
|
||||||
export interface DeskDocument {
|
export interface DeskDocument {
|
||||||
id?: Identifier | null;
|
id?: Identifier | null;
|
||||||
@@ -49,6 +52,7 @@ interface NavigatorSnapshot {
|
|||||||
ordinal?: number | null;
|
ordinal?: number | null;
|
||||||
width?: number | null;
|
width?: number | null;
|
||||||
height?: number | null;
|
height?: number | null;
|
||||||
|
contentType?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OverlayOriginHint = {
|
type OverlayOriginHint = {
|
||||||
@@ -124,8 +128,8 @@ interface DesktopWorkspaceProps {
|
|||||||
onDocumentStackSelect?: (docIds: Identifier[]) => void;
|
onDocumentStackSelect?: (docIds: Identifier[]) => void;
|
||||||
onPromoteSelection?: (...args: unknown[]) => void;
|
onPromoteSelection?: (...args: unknown[]) => void;
|
||||||
onAssignTagToDocument?: (...args: unknown[]) => void;
|
onAssignTagToDocument?: (...args: unknown[]) => void;
|
||||||
ensureAssetUrl?: (...args: unknown[]) => Promise<unknown> | unknown;
|
ensureAssetUrl?: EnsureAssetUrl;
|
||||||
getDocumentAsset?: (...args: unknown[]) => unknown;
|
getDocumentAsset?: GetAsset;
|
||||||
activeTagIds?: Array<Identifier | null>;
|
activeTagIds?: Array<Identifier | null>;
|
||||||
selectedDocumentIds?: Identifier[];
|
selectedDocumentIds?: Identifier[];
|
||||||
onClearSelection?: () => void;
|
onClearSelection?: () => void;
|
||||||
@@ -193,6 +197,8 @@ interface DesktopWorkspaceViewProps {
|
|||||||
|
|
||||||
const DEBUG_DRAG = false;
|
const DEBUG_DRAG = false;
|
||||||
const DEBUG_FOCUS = false;
|
const DEBUG_FOCUS = false;
|
||||||
|
const defaultGetDocumentAsset: GetAsset = () => null;
|
||||||
|
|
||||||
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||||
documents = [],
|
documents = [],
|
||||||
searchResults = null,
|
searchResults = null,
|
||||||
@@ -202,7 +208,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
onPromoteSelection = null,
|
onPromoteSelection = null,
|
||||||
onAssignTagToDocument = null,
|
onAssignTagToDocument = null,
|
||||||
ensureAssetUrl = null,
|
ensureAssetUrl = null,
|
||||||
getDocumentAsset = () => null,
|
getDocumentAsset = defaultGetDocumentAsset,
|
||||||
activeTagIds = [],
|
activeTagIds = [],
|
||||||
selectedDocumentIds = [],
|
selectedDocumentIds = [],
|
||||||
onClearSelection = null,
|
onClearSelection = null,
|
||||||
@@ -210,6 +216,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
onCloseDetailPanel = null,
|
onCloseDetailPanel = null,
|
||||||
tenantId = null,
|
tenantId = null,
|
||||||
viewId = 'default',
|
viewId = 'default',
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
}) => {
|
}) => {
|
||||||
const items = useMemo<DeskDocument[]>(
|
const items = useMemo<DeskDocument[]>(
|
||||||
() => (searchResults ? searchResults : documents),
|
() => (searchResults ? searchResults : documents),
|
||||||
@@ -217,6 +225,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const allowLayoutPersistence = Boolean(tenantId && viewId && viewId.startsWith('folder:'));
|
const allowLayoutPersistence = Boolean(tenantId && viewId && viewId.startsWith('folder:'));
|
||||||
|
const previewEntryMap = previewEntries instanceof Map ? previewEntries : null;
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const itemRefs = useRef<Map<string, HTMLDivElement | null>>(new Map());
|
const itemRefs = useRef<Map<string, HTMLDivElement | null>>(new Map());
|
||||||
@@ -226,9 +235,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
const [overlayOriginTransform, setOverlayOriginTransform] = useState<OverlayOriginTransform | null>(
|
const [overlayOriginTransform, setOverlayOriginTransform] = useState<OverlayOriginTransform | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [previewSnapshots, setPreviewSnapshots] = useState<Map<string, NavigatorSnapshot>>(
|
const [overlaySource, setOverlaySource] = useState<OverlaySource | null>(null);
|
||||||
() => new Map(),
|
const [, setPreviewSnapshots] = useState<Map<string, NavigatorSnapshot>>(() => new Map());
|
||||||
);
|
|
||||||
const [docSizeVersion, setDocSizeVersion] = useState(0);
|
const [docSizeVersion, setDocSizeVersion] = useState(0);
|
||||||
const docSizeMapRef = useRef<Map<string, DocumentSizeInfo>>(new Map());
|
const docSizeMapRef = useRef<Map<string, DocumentSizeInfo>>(new Map());
|
||||||
const ensureDocumentSize = useCallback((doc: DeskDocument | null): DocumentSizeInfo | null => {
|
const ensureDocumentSize = useCallback((doc: DeskDocument | null): DocumentSizeInfo | null => {
|
||||||
@@ -444,7 +452,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
if (!doc) {
|
if (!doc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolveDocumentAssetUrl(doc, 'preview', {
|
resolveDocumentAssetUrl(doc, 'thumbnail', {
|
||||||
ensureAssetUrl,
|
ensureAssetUrl,
|
||||||
getAsset: getDocumentAsset,
|
getAsset: getDocumentAsset,
|
||||||
});
|
});
|
||||||
@@ -546,30 +554,77 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
}
|
}
|
||||||
}, [items, previewMetadata]);
|
}, [items, previewMetadata]);
|
||||||
|
|
||||||
const overlayDisplay = useMemo<OverlayDisplay | null>(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
if (!overlayDocId) {
|
if (!overlayDocId) {
|
||||||
return null;
|
setOverlaySource(null);
|
||||||
}
|
return () => {
|
||||||
const snapshot = previewSnapshots.get(overlayDocId);
|
cancelled = true;
|
||||||
if (!snapshot || !snapshot.url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const doc = documentLookup.get(overlayDocId);
|
|
||||||
const alt = snapshot.alt || (doc?.title as string | undefined);
|
|
||||||
return {
|
|
||||||
url: snapshot.url,
|
|
||||||
alt,
|
|
||||||
canGoPrev: snapshot.canGoPrev,
|
|
||||||
canGoNext: snapshot.canGoNext,
|
|
||||||
goPrev: snapshot.goPrev,
|
|
||||||
goNext: snapshot.goNext,
|
|
||||||
};
|
};
|
||||||
}, [overlayDocId, previewSnapshots, documentLookup]);
|
}
|
||||||
|
|
||||||
|
const doc = documentLookup.get(overlayDocId) || null;
|
||||||
|
const docIdentifier = doc?.id ?? null;
|
||||||
|
if (!docIdentifier || !doc) {
|
||||||
|
setOverlaySource(null);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const docContentType = doc?.content_type ?? null;
|
||||||
|
const versionContentType = (doc?.current_version as { version?: { content_type?: string | null } } | null)?.version?.content_type ?? null;
|
||||||
|
|
||||||
|
const applyEntry = (entry?: PreviewEntryLike | null) => {
|
||||||
|
if (!entry?.url) {
|
||||||
|
setOverlaySource(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOverlaySource({
|
||||||
|
url: entry.url,
|
||||||
|
alt: doc.title as string | undefined,
|
||||||
|
contentType: entry.contentType || docContentType || versionContentType || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedEntry = previewEntryMap?.get(docIdentifier) || null;
|
||||||
|
if (cachedEntry?.url) {
|
||||||
|
applyEntry(cachedEntry);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ensureDownloadUrl) {
|
||||||
|
setOverlaySource(null);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDownloadUrl(docIdentifier)
|
||||||
|
.then((entry) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyEntry(entry);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setOverlaySource(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [overlayDocId, documentLookup, previewEntryMap, ensureDownloadUrl]);
|
||||||
|
|
||||||
const closeOverlay = useCallback(() => {
|
const closeOverlay = useCallback(() => {
|
||||||
setOverlayDocId(null);
|
setOverlayDocId(null);
|
||||||
setOverlayOriginRect(null);
|
setOverlayOriginRect(null);
|
||||||
setOverlayOriginTransform(null);
|
setOverlayOriginTransform(null);
|
||||||
|
setOverlaySource(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -611,24 +666,32 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
}
|
}
|
||||||
}, [draggingId, items, setDraggingId]);
|
}, [draggingId, items, setDraggingId]);
|
||||||
|
|
||||||
|
const overlayDisplay = useMemo<OverlayDisplay | null>(() => {
|
||||||
|
if (!overlaySource) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...overlaySource,
|
||||||
|
canGoPrev: false,
|
||||||
|
canGoNext: false,
|
||||||
|
};
|
||||||
|
}, [overlaySource]);
|
||||||
|
|
||||||
const openOverlayForDoc = useCallback(
|
const openOverlayForDoc = useCallback(
|
||||||
(docId: Identifier | null, originInfo: OverlayOriginHint | null = null) => {
|
(docId: Identifier | null, originInfo: OverlayOriginHint | null = null) => {
|
||||||
if (!docId) {
|
if (!docId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const docKey = String(docId);
|
const docKey = String(docId);
|
||||||
const snapshot = previewSnapshots.get(docKey);
|
|
||||||
if (!snapshot || !snapshot.url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const container = itemRefs.current.get(docKey);
|
const container = itemRefs.current.get(docKey);
|
||||||
const imageNode = container
|
if (!container) {
|
||||||
? container.querySelector<HTMLImageElement>('.desk-item__card img')
|
return;
|
||||||
: null;
|
}
|
||||||
if (!container || !imageNode) {
|
const imageNode = container.querySelector<HTMLImageElement>('.desk-item__card img');
|
||||||
|
const rect = (imageNode || container).getBoundingClientRect();
|
||||||
|
if (!rect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rect = imageNode.getBoundingClientRect();
|
|
||||||
let originTransform = null;
|
let originTransform = null;
|
||||||
if (originInfo) {
|
if (originInfo) {
|
||||||
const { rotation = 0, scale = 1, width: originWidth, height: originHeight } = originInfo;
|
const { rotation = 0, scale = 1, width: originWidth, height: originHeight } = originInfo;
|
||||||
@@ -666,7 +729,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
bringToFront,
|
bringToFront,
|
||||||
previewSnapshots,
|
|
||||||
itemRefs,
|
itemRefs,
|
||||||
setOverlayOriginTransform,
|
setOverlayOriginTransform,
|
||||||
ensureDocumentSize,
|
ensureDocumentSize,
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ interface UseDeskWorkspacePropsArgs {
|
|||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
activeCorrespondentFilters?: Identifier[];
|
activeCorrespondentFilters?: Identifier[];
|
||||||
selectedFolder?: Identifier | string | null;
|
selectedFolder?: Identifier | string | null;
|
||||||
|
previewEntries?: Map<Identifier, { url?: string | null; contentType?: string | null }>;
|
||||||
|
ensureDownloadUrl?: (documentId: Identifier, options?: { force?: boolean }) => Promise<{ url?: string | null; contentType?: string | null } | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useDeskWorkspaceProps = ({
|
const useDeskWorkspaceProps = ({
|
||||||
@@ -108,6 +110,8 @@ const useDeskWorkspaceProps = ({
|
|||||||
searchQuery = '',
|
searchQuery = '',
|
||||||
activeCorrespondentFilters = [],
|
activeCorrespondentFilters = [],
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
}: UseDeskWorkspacePropsArgs) => {
|
}: UseDeskWorkspacePropsArgs) => {
|
||||||
const handleDeskDocumentStackSelect: DeskDocumentStackSelectHandler = useCallback(
|
const handleDeskDocumentStackSelect: DeskDocumentStackSelectHandler = useCallback(
|
||||||
(docIds) => {
|
(docIds) => {
|
||||||
@@ -200,6 +204,8 @@ const useDeskWorkspaceProps = ({
|
|||||||
onMoveDocumentsToFolder: moveDocumentsToFolder,
|
onMoveDocumentsToFolder: moveDocumentsToFolder,
|
||||||
searchIncludeDescendants,
|
searchIncludeDescendants,
|
||||||
onToggleSearchIncludeDescendants: toggleSearchIncludeDescendants,
|
onToggleSearchIncludeDescendants: toggleSearchIncludeDescendants,
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
documents,
|
documents,
|
||||||
@@ -240,6 +246,8 @@ const useDeskWorkspaceProps = ({
|
|||||||
moveDocumentsToFolder,
|
moveDocumentsToFolder,
|
||||||
searchIncludeDescendants,
|
searchIncludeDescendants,
|
||||||
toggleSearchIncludeDescendants,
|
toggleSearchIncludeDescendants,
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import React, { CSSProperties, useEffect, useRef, useState } from 'react';
|
import React, { CSSProperties, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from '../ui/icons';
|
import { ArrowLeftIcon, ArrowRightIcon } from '../ui/icons';
|
||||||
import { clamp } from '../utils/math';
|
import { clamp } from '../utils/math';
|
||||||
|
import PdfViewer from '../preview/PdfViewer';
|
||||||
|
|
||||||
type PreviewNavigator = {
|
type PreviewNavigator = {
|
||||||
url: string;
|
url: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
|
contentType?: string | null;
|
||||||
|
pageNumber?: number | null;
|
||||||
canGoPrev?: boolean;
|
canGoPrev?: boolean;
|
||||||
canGoNext?: boolean;
|
canGoNext?: boolean;
|
||||||
goPrev?: () => void;
|
goPrev?: () => void;
|
||||||
@@ -20,6 +23,22 @@ interface PreviewZoomOverlayProps {
|
|||||||
|
|
||||||
type NaturalSize = { width: number | null; height: number | null };
|
type NaturalSize = { width: number | null; height: number | null };
|
||||||
type FocusPoint = { xRatio: number; yRatio: number } | null;
|
type FocusPoint = { xRatio: number; yRatio: number } | null;
|
||||||
|
type DisplayKind = 'image' | 'pdf';
|
||||||
|
|
||||||
|
const determineDisplayKind = (entry?: PreviewNavigator | null): DisplayKind => {
|
||||||
|
const type = entry?.contentType?.toLowerCase?.() || '';
|
||||||
|
if (type.includes('pdf')) {
|
||||||
|
return 'pdf';
|
||||||
|
}
|
||||||
|
if (type.startsWith('image/')) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
const url = entry?.url?.toLowerCase?.() || '';
|
||||||
|
if (url.endsWith('.pdf')) {
|
||||||
|
return 'pdf';
|
||||||
|
}
|
||||||
|
return 'image';
|
||||||
|
};
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
@@ -35,7 +54,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
const [isBackdropVisible, setBackdropVisible] = useState(false);
|
const [isBackdropVisible, setBackdropVisible] = useState(false);
|
||||||
const [displaySnapshot, setDisplaySnapshot] = useState<PreviewNavigator | null>(null);
|
const [displaySnapshot, setDisplaySnapshot] = useState<PreviewNavigator | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
const mediaRef = useRef<HTMLImageElement | null>(null);
|
||||||
const focusRef = useRef<FocusPoint>(null);
|
const focusRef = useRef<FocusPoint>(null);
|
||||||
const previouslyFocusedRef = useRef<HTMLElement | null>(null);
|
const previouslyFocusedRef = useRef<HTMLElement | null>(null);
|
||||||
const visibilityTimerRef = useRef<number | null>(null);
|
const visibilityTimerRef = useRef<number | null>(null);
|
||||||
@@ -47,6 +66,10 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
}
|
}
|
||||||
}, [display]);
|
}, [display]);
|
||||||
|
|
||||||
|
const activeDisplay = open && display?.url ? display : displaySnapshot;
|
||||||
|
const displayKind = useMemo(() => determineDisplayKind(activeDisplay), [activeDisplay]);
|
||||||
|
const isPdfDisplay = displayKind === 'pdf';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visibilityTimerRef.current) {
|
if (visibilityTimerRef.current) {
|
||||||
clearTimeout(visibilityTimerRef.current);
|
clearTimeout(visibilityTimerRef.current);
|
||||||
@@ -94,6 +117,9 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
}, [isPdfDisplay, open, activeDisplay?.url]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsNativeScale(false);
|
setIsNativeScale(false);
|
||||||
setNaturalSize({ width: null, height: null });
|
setNaturalSize({ width: null, height: null });
|
||||||
@@ -103,7 +129,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
scrollEl.scrollLeft = 0;
|
scrollEl.scrollLeft = 0;
|
||||||
scrollEl.scrollTop = 0;
|
scrollEl.scrollTop = 0;
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open, activeDisplay?.url, displayKind]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -112,13 +138,13 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scrollEl = scrollRef.current;
|
const scrollEl = scrollRef.current;
|
||||||
const imageEl = imageRef.current;
|
const mediaEl = mediaRef.current;
|
||||||
if (!scrollEl || !imageEl) {
|
if (!scrollEl || !mediaEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageWidth = imageEl.naturalWidth || imageEl.clientWidth;
|
const imageWidth = naturalSize.width || mediaEl.clientWidth;
|
||||||
const imageHeight = imageEl.naturalHeight || imageEl.clientHeight;
|
const imageHeight = naturalSize.height || mediaEl.clientHeight;
|
||||||
if (!(imageWidth > 0 && imageHeight > 0)) {
|
if (!(imageWidth > 0 && imageHeight > 0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -145,8 +171,6 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
previouslyFocusedRef.current = active instanceof HTMLElement ? active : null;
|
previouslyFocusedRef.current = active instanceof HTMLElement ? active : null;
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const activeDisplay = open && display?.url ? display : displaySnapshot;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeDisplay?.url) {
|
if (!activeDisplay?.url) {
|
||||||
return;
|
return;
|
||||||
@@ -222,10 +246,13 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleZoomAtPoint = (clientX: number, clientY: number) => {
|
const toggleZoomAtPoint = (clientX: number, clientY: number) => {
|
||||||
const img = imageRef.current;
|
if (isPdfDisplay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const media = mediaRef.current;
|
||||||
setIsNativeScale((current) => {
|
setIsNativeScale((current) => {
|
||||||
if (!current && img) {
|
if (!current && media) {
|
||||||
const rect = img.getBoundingClientRect();
|
const rect = media.getBoundingClientRect();
|
||||||
const xRatio = rect.width > 0 ? (clientX - rect.left) / rect.width : 0.5;
|
const xRatio = rect.width > 0 ? (clientX - rect.left) / rect.width : 0.5;
|
||||||
const yRatio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0.5;
|
const yRatio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0.5;
|
||||||
focusRef.current = {
|
focusRef.current = {
|
||||||
@@ -239,22 +266,24 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageClick = (event: React.MouseEvent<HTMLImageElement>) => {
|
const handleContentClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
if (isPdfDisplay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
toggleZoomAtPoint(event.clientX ?? 0, event.clientY ?? 0);
|
toggleZoomAtPoint(event.clientX ?? 0, event.clientY ?? 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!renderBackdrop || !activeDisplay?.url) {
|
const shouldRender = renderBackdrop && Boolean(activeDisplay?.url);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const effectiveDisplay = activeDisplay;
|
const stageClassName = isPdfDisplay
|
||||||
const navVisible = Boolean(effectiveDisplay?.canGoPrev || effectiveDisplay?.canGoNext);
|
? 'preview-zoom__stage preview-zoom__stage--pdf'
|
||||||
const stageClassName = 'preview-zoom__stage';
|
: 'preview-zoom__stage';
|
||||||
|
|
||||||
const containerClassName = [
|
const containerClassName = [
|
||||||
'preview-zoom__scroll',
|
'preview-zoom__scroll',
|
||||||
isNativeScale ? 'preview-zoom__scroll--native' : '',
|
isNativeScale ? 'preview-zoom__scroll--native' : null,
|
||||||
|
isPdfDisplay ? 'preview-zoom__scroll--pdf' : null,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
@@ -266,7 +295,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
const imageStyle: CSSProperties = isNativeScale
|
const contentStyle: CSSProperties = isNativeScale
|
||||||
? {
|
? {
|
||||||
cursor: 'zoom-out',
|
cursor: 'zoom-out',
|
||||||
width: naturalSize.width ? `${naturalSize.width}px` : 'auto',
|
width: naturalSize.width ? `${naturalSize.width}px` : 'auto',
|
||||||
@@ -282,6 +311,13 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
touchAction: 'manipulation',
|
touchAction: 'manipulation',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!shouldRender) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveDisplay = activeDisplay;
|
||||||
|
const navVisible = Boolean(effectiveDisplay?.canGoPrev || effectiveDisplay?.canGoNext);
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
(
|
(
|
||||||
<div
|
<div
|
||||||
@@ -290,19 +326,37 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Enlarged document preview"
|
aria-label="Enlarged document preview"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={stageClassName}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={stageClassName} onKeyDown={handleKeyDown}>
|
|
||||||
<div
|
<div
|
||||||
className={containerClassName}
|
className={containerClassName}
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
|
{isPdfDisplay ? (
|
||||||
|
<div className="preview-zoom__pdf">
|
||||||
|
<PdfViewer
|
||||||
|
src={effectiveDisplay.url}
|
||||||
|
title={effectiveDisplay.alt || 'Document preview'}
|
||||||
|
className="preview-zoom__pdf-viewer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<img
|
<img
|
||||||
src={effectiveDisplay.url}
|
src={effectiveDisplay.url}
|
||||||
alt={effectiveDisplay.alt || 'Document preview'}
|
alt={effectiveDisplay.alt || 'Document preview'}
|
||||||
className="preview-zoom__image"
|
className="preview-zoom__image"
|
||||||
ref={imageRef}
|
ref={(node) => {
|
||||||
|
mediaRef.current = node;
|
||||||
|
}}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
onLoad={(event) => {
|
onLoad={(event) => {
|
||||||
setNaturalSize({
|
setNaturalSize({
|
||||||
@@ -310,9 +364,10 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
height: event.currentTarget.naturalHeight || null,
|
height: event.currentTarget.naturalHeight || null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onClick={handleImageClick}
|
onClick={handleContentClick}
|
||||||
style={imageStyle}
|
style={contentStyle}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{navVisible ? (
|
{navVisible ? (
|
||||||
<div className="preview-zoom__nav">
|
<div className="preview-zoom__nav">
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
type Identifier = string | number;
|
type Identifier = string | number;
|
||||||
|
|
||||||
|
interface PreviewEntryLike {
|
||||||
|
url?: string | null;
|
||||||
|
contentType?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Breadcrumb {
|
export interface Breadcrumb {
|
||||||
id?: Identifier;
|
id?: Identifier;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -65,6 +70,8 @@ export interface UseDocumentsPanelPropsArgs {
|
|||||||
handleBulkSelectionReanalyze?: (...args: unknown[]) => void;
|
handleBulkSelectionReanalyze?: (...args: unknown[]) => void;
|
||||||
folderOptions?: unknown[];
|
folderOptions?: unknown[];
|
||||||
moveDocumentsToFolder?: (...args: unknown[]) => void;
|
moveDocumentsToFolder?: (...args: unknown[]) => void;
|
||||||
|
previewEntries?: Map<Identifier, PreviewEntryLike>;
|
||||||
|
ensureDownloadUrl?: (documentId: Identifier, options?: { force?: boolean }) => Promise<PreviewEntryLike | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentsPanelProps = ReturnType<typeof useDocumentsPanelProps>;
|
export type DocumentsPanelProps = ReturnType<typeof useDocumentsPanelProps>;
|
||||||
@@ -119,6 +126,8 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
|||||||
handleBulkSelectionReanalyze,
|
handleBulkSelectionReanalyze,
|
||||||
folderOptions,
|
folderOptions,
|
||||||
moveDocumentsToFolder,
|
moveDocumentsToFolder,
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
@@ -173,6 +182,8 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
|||||||
onBulkReanalyze: handleBulkSelectionReanalyze,
|
onBulkReanalyze: handleBulkSelectionReanalyze,
|
||||||
folderOptions,
|
folderOptions,
|
||||||
onMoveDocumentsToFolder: moveDocumentsToFolder,
|
onMoveDocumentsToFolder: moveDocumentsToFolder,
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
activeCorrespondentFilters,
|
activeCorrespondentFilters,
|
||||||
@@ -223,6 +234,8 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
|||||||
ensureAssetUrl,
|
ensureAssetUrl,
|
||||||
getDocumentAsset,
|
getDocumentAsset,
|
||||||
folderOptions,
|
folderOptions,
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import DocumentsGrid from '../DocumentsGrid';
|
|||||||
import DocumentsList from '../DocumentsList';
|
import DocumentsList from '../DocumentsList';
|
||||||
import { isTagTransferEvent } from '../tagTransfer';
|
import { isTagTransferEvent } from '../tagTransfer';
|
||||||
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
|
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
|
||||||
import { useAssetNavigator } from '../../hooks/useAssetNavigator';
|
|
||||||
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
|
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
|
||||||
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
|
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
|
||||||
|
|
||||||
@@ -22,6 +21,8 @@ interface DocumentsPanelProps {
|
|||||||
|
|
||||||
const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
|
const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
|
||||||
|
|
||||||
|
export type PreviewEntryLike = { url?: string | null; contentType?: string | null };
|
||||||
|
|
||||||
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
@@ -55,6 +56,8 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
viewMode = 'list',
|
viewMode = 'list',
|
||||||
onViewModeChange,
|
onViewModeChange,
|
||||||
showHeader = true,
|
showHeader = true,
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
selectedEntries,
|
selectedEntries,
|
||||||
@@ -65,6 +68,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
} = useWorkspaceSelectionContext();
|
} = useWorkspaceSelectionContext();
|
||||||
const showingSearchResults = searchResults !== null;
|
const showingSearchResults = searchResults !== null;
|
||||||
const rows = showingSearchResults ? searchResults : documents;
|
const rows = showingSearchResults ? searchResults : documents;
|
||||||
|
const previewEntryMap = previewEntries instanceof Map ? previewEntries : null;
|
||||||
|
|
||||||
const currentFolderId = useMemo(() => {
|
const currentFolderId = useMemo(() => {
|
||||||
if (showingSearchResults) {
|
if (showingSearchResults) {
|
||||||
@@ -138,7 +142,10 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
const isGridView = viewMode === 'grid';
|
const isGridView = viewMode === 'grid';
|
||||||
const isDeskView = viewMode === 'desk';
|
const isDeskView = viewMode === 'desk';
|
||||||
|
|
||||||
const [previewDocId, setPreviewDocId] = useState(null);
|
type Identifier = string | number;
|
||||||
|
type ZoomSource = { url: string; alt?: string | null; contentType?: string | null };
|
||||||
|
|
||||||
|
const [previewDocId, setPreviewDocId] = useState<Identifier | null>(null);
|
||||||
|
|
||||||
const previewDoc = useMemo(() => {
|
const previewDoc = useMemo(() => {
|
||||||
if (!previewDocId) {
|
if (!previewDocId) {
|
||||||
@@ -153,38 +160,69 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
}
|
}
|
||||||
}, [previewDocId, previewDoc]);
|
}, [previewDocId, previewDoc]);
|
||||||
|
|
||||||
const previewNavigator = useAssetNavigator({
|
const [previewZoomSource, setPreviewZoomSource] = useState<ZoomSource | null>(null);
|
||||||
document: previewDoc,
|
const zoomDisplay = previewZoomSource;
|
||||||
assetType: 'preview',
|
|
||||||
ensureAssetUrl,
|
useEffect(() => {
|
||||||
getAsset: getDocumentAsset,
|
let cancelled = false;
|
||||||
prefetch: 3,
|
if (!previewDocId || !previewDoc) {
|
||||||
|
setPreviewZoomSource(null);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const docContentType = previewDoc.content_type;
|
||||||
|
const versionContentType = previewDoc.current_version?.version?.content_type;
|
||||||
|
const contentFallback = docContentType || versionContentType || null;
|
||||||
|
|
||||||
|
const applyEntry = (entry?: PreviewEntryLike | null) => {
|
||||||
|
if (!entry?.url) {
|
||||||
|
setPreviewZoomSource(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPreviewZoomSource({
|
||||||
|
url: entry.url,
|
||||||
|
alt: previewDoc.title,
|
||||||
|
contentType: entry.contentType || contentFallback || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedEntry = previewEntryMap?.get(previewDocId) || null;
|
||||||
|
if (cachedEntry?.url) {
|
||||||
|
applyEntry(cachedEntry);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ensureDownloadUrl) {
|
||||||
|
setPreviewZoomSource(null);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDownloadUrl(previewDocId)
|
||||||
|
.then((entry) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyEntry(entry);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setPreviewZoomSource(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
return () => {
|
||||||
currentUrl: previewUrl,
|
cancelled = true;
|
||||||
canGoPrev: previewCanGoPrev,
|
|
||||||
canGoNext: previewCanGoNext,
|
|
||||||
goPrev: previewGoPrev,
|
|
||||||
goNext: previewGoNext,
|
|
||||||
} = previewNavigator;
|
|
||||||
|
|
||||||
const previewDisplay = useMemo(() => {
|
|
||||||
if (!previewDoc || !previewUrl) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
url: previewUrl,
|
|
||||||
alt: previewDoc.title,
|
|
||||||
canGoPrev: Boolean(previewCanGoPrev),
|
|
||||||
canGoNext: Boolean(previewCanGoNext),
|
|
||||||
goPrev: previewGoPrev,
|
|
||||||
goNext: previewGoNext,
|
|
||||||
};
|
};
|
||||||
}, [previewDoc, previewUrl, previewCanGoPrev, previewCanGoNext, previewGoPrev, previewGoNext]);
|
}, [previewDocId, previewDoc, previewEntryMap, ensureDownloadUrl]);
|
||||||
|
|
||||||
const closePreviewOverlay = useCallback(() => {
|
const closePreviewOverlay = useCallback(() => {
|
||||||
setPreviewDocId(null);
|
setPreviewDocId(null);
|
||||||
|
setPreviewZoomSource(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDocumentPreviewZoom = useCallback(
|
const handleDocumentPreviewZoom = useCallback(
|
||||||
@@ -192,13 +230,12 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
if (!doc || !doc.id) {
|
if (!doc || !doc.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const previewAsset = getDocumentAsset(doc, 'preview');
|
if (!ensureDownloadUrl && !(previewEntryMap?.get(doc.id)?.url)) {
|
||||||
if (!previewAsset) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPreviewDocId(doc.id);
|
setPreviewDocId(doc.id);
|
||||||
},
|
},
|
||||||
[getDocumentAsset],
|
[ensureDownloadUrl, previewEntryMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDocumentActivate = useCallback(
|
const handleDocumentActivate = useCallback(
|
||||||
@@ -724,8 +761,8 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<PreviewZoomOverlay
|
<PreviewZoomOverlay
|
||||||
open={Boolean(previewDocId)}
|
open={Boolean(zoomDisplay?.url)}
|
||||||
display={previewDisplay}
|
display={zoomDisplay}
|
||||||
onClose={closePreviewOverlay}
|
onClose={closePreviewOverlay}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ const useDocumentsWorkspace = ({
|
|||||||
previewEntries,
|
previewEntries,
|
||||||
previewDocuments,
|
previewDocuments,
|
||||||
ensurePreviewData,
|
ensurePreviewData,
|
||||||
|
ensureDownloadUrl,
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
closeDocumentPreview,
|
closeDocumentPreview,
|
||||||
resetPreviewState,
|
resetPreviewState,
|
||||||
@@ -1452,6 +1453,8 @@ const useDocumentsWorkspace = ({
|
|||||||
folderOptions,
|
folderOptions,
|
||||||
moveDocumentsToFolder,
|
moveDocumentsToFolder,
|
||||||
selectFolder,
|
selectFolder,
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sidebarProps = useSidebarProps({
|
const sidebarProps = useSidebarProps({
|
||||||
@@ -1539,6 +1542,8 @@ const useDocumentsWorkspace = ({
|
|||||||
searchQuery,
|
searchQuery,
|
||||||
activeCorrespondentFilters,
|
activeCorrespondentFilters,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
|
previewEntries,
|
||||||
|
ensureDownloadUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react';
|
|||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
||||||
import { DownloadIcon } from '../ui/icons';
|
import { DownloadIcon } from '../ui/icons';
|
||||||
|
import PdfViewer from './PdfViewer';
|
||||||
|
|
||||||
interface DocumentLike {
|
interface DocumentLike {
|
||||||
id?: string | number;
|
id?: string | number;
|
||||||
@@ -87,11 +88,13 @@ const DocumentViewerLayout = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
|
const documentTitle = document.title
|
||||||
|
|| document.filename
|
||||||
|
|| document.original_name;
|
||||||
return (
|
return (
|
||||||
<iframe
|
<PdfViewer
|
||||||
src={previewEntry.url}
|
src={previewEntry.url}
|
||||||
title={`Preview of ${document.title}`}
|
title={documentTitle ? `Preview of ${documentTitle}` : undefined}
|
||||||
className="document-viewer__object"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { createDocumentActionState } from '../documents/documentActions';
|
|||||||
import { resolveDocumentAssetUrl } from '../asset_manager';
|
import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||||
import PanelHeader from '../ui/PanelHeader';
|
import PanelHeader from '../ui/PanelHeader';
|
||||||
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
||||||
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
|
||||||
import DocumentViewerLayout from './DocumentViewerLayout';
|
import DocumentViewerLayout from './DocumentViewerLayout';
|
||||||
import useViewerLayoutMode from './useViewerLayoutMode';
|
import useViewerLayoutMode from './useViewerLayoutMode';
|
||||||
import { usePanelResizeBindings } from '../app/PanelManagerContext';
|
import { usePanelResizeBindings } from '../app/PanelManagerContext';
|
||||||
@@ -33,8 +32,13 @@ import { usePanelResizeBindings } from '../app/PanelManagerContext';
|
|||||||
interface DocumentLike {
|
interface DocumentLike {
|
||||||
id?: string | number;
|
id?: string | number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
content_type?: string | null;
|
||||||
issued_at?: string | null;
|
issued_at?: string | null;
|
||||||
correspondents?: Array<{ id?: string | number; name?: string }>;
|
correspondents?: Array<{ id?: string | number; name?: string }>;
|
||||||
|
current_version?: {
|
||||||
|
version_number?: number;
|
||||||
|
version?: { content_type?: string | null } | null;
|
||||||
|
} | null;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,18 +249,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
|
const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
|
||||||
const previewNavigator = useAssetNavigator({
|
|
||||||
document,
|
|
||||||
assetType: 'preview',
|
|
||||||
ensureAssetUrl,
|
|
||||||
getAsset: getDocumentAsset,
|
|
||||||
prefetch: 3,
|
|
||||||
});
|
|
||||||
const navigatorUrl = previewNavigator?.currentUrl;
|
|
||||||
const navigatorCanGoPrev = Boolean(previewNavigator?.canGoPrev);
|
|
||||||
const navigatorCanGoNext = Boolean(previewNavigator?.canGoNext);
|
|
||||||
const navigatorGoPrev = previewNavigator?.goPrev;
|
|
||||||
const navigatorGoNext = previewNavigator?.goNext;
|
|
||||||
|
|
||||||
const handleZoomOpen = useCallback(() => {
|
const handleZoomOpen = useCallback(() => {
|
||||||
if (!previewEntry?.url) {
|
if (!previewEntry?.url) {
|
||||||
@@ -360,34 +352,18 @@ const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(n
|
|||||||
}, [breadcrumbs, handleBreadcrumbNavigate]);
|
}, [breadcrumbs, handleBreadcrumbNavigate]);
|
||||||
|
|
||||||
const zoomDisplay = useMemo(() => {
|
const zoomDisplay = useMemo(() => {
|
||||||
if (navigatorUrl && document) {
|
if (!previewEntry?.url || !document) {
|
||||||
return {
|
return null;
|
||||||
url: navigatorUrl,
|
|
||||||
alt: document.title,
|
|
||||||
canGoPrev: navigatorCanGoPrev,
|
|
||||||
canGoNext: navigatorCanGoNext,
|
|
||||||
goPrev: navigatorCanGoPrev ? navigatorGoPrev : undefined,
|
|
||||||
goNext: navigatorCanGoNext ? navigatorGoNext : undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (previewEntry?.url && document) {
|
const docContentType = document.content_type;
|
||||||
|
const versionContentType = document.current_version?.version?.content_type;
|
||||||
|
const normalizedContentType = previewEntry.contentType || docContentType || versionContentType || null;
|
||||||
return {
|
return {
|
||||||
url: previewEntry.url,
|
url: previewEntry.url,
|
||||||
alt: document.title,
|
alt: document.title,
|
||||||
canGoPrev: false,
|
contentType: normalizedContentType || undefined,
|
||||||
canGoNext: false,
|
|
||||||
};
|
};
|
||||||
}
|
}, [previewEntry?.url, previewEntry?.contentType, document]);
|
||||||
return null;
|
|
||||||
}, [
|
|
||||||
navigatorUrl,
|
|
||||||
navigatorCanGoPrev,
|
|
||||||
navigatorCanGoNext,
|
|
||||||
navigatorGoPrev,
|
|
||||||
navigatorGoNext,
|
|
||||||
previewEntry?.url,
|
|
||||||
document,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const headerActions = createDocumentViewerHeaderActions({
|
const headerActions = createDocumentViewerHeaderActions({
|
||||||
document,
|
document,
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
import React, {
|
||||||
|
Fragment,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import type { JSX, MutableRefObject, RefObject } from 'react';
|
||||||
|
import {
|
||||||
|
GlobalWorkerOptions,
|
||||||
|
getDocument,
|
||||||
|
} from 'pdfjs-dist';
|
||||||
|
import type {
|
||||||
|
PDFDocumentLoadingTask,
|
||||||
|
PDFDocumentProxy,
|
||||||
|
RenderTask,
|
||||||
|
} from 'pdfjs-dist/types/src/display/api';
|
||||||
|
import { TextLayerBuilder } from 'pdfjs-dist/web/pdf_viewer.mjs';
|
||||||
|
GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
|
import.meta.url,
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
interface PdfViewerProps {
|
||||||
|
src: string;
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderStatus = 'idle' | 'loading' | 'ready' | 'error';
|
||||||
|
|
||||||
|
interface PageDescriptor {
|
||||||
|
number: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_RENDER_WIDTH = 1200;
|
||||||
|
const MAX_RENDER_WIDTH = 2200;
|
||||||
|
const RERENDER_DELTA = 48;
|
||||||
|
|
||||||
|
const resolvePdfWasmBaseUrl = (): string => {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const base = document.baseURI || (typeof window !== 'undefined' ? window.location.href : '/');
|
||||||
|
return new URL('./pdfjs/wasm/', base).toString();
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined' && window.location) {
|
||||||
|
return new URL('./pdfjs/wasm/', window.location.href).toString();
|
||||||
|
}
|
||||||
|
return '/pdfjs/wasm/';
|
||||||
|
};
|
||||||
|
|
||||||
|
const PdfViewer = ({ src, title, className }: PdfViewerProps): JSX.Element => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [viewportWidth, setViewportWidth] = useState(0);
|
||||||
|
const [renderWidth, setRenderWidth] = useState(0);
|
||||||
|
const [status, setStatus] = useState<RenderStatus>('idle');
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [pages, setPages] = useState<PageDescriptor[]>([]);
|
||||||
|
const [pixelRatio, setPixelRatio] = useState(1);
|
||||||
|
const pdfRef = useRef<PDFDocumentProxy | null>(null);
|
||||||
|
const wasmUrlRef = useRef<string | null>(null);
|
||||||
|
const ensureWasmUrl = useCallback(() => {
|
||||||
|
if (!wasmUrlRef.current) {
|
||||||
|
wasmUrlRef.current = resolvePdfWasmBaseUrl();
|
||||||
|
}
|
||||||
|
return wasmUrlRef.current;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateWidth = () => {
|
||||||
|
const measurementTarget = (
|
||||||
|
container.closest('.document-viewer__viewport') as HTMLElement | null
|
||||||
|
) || container.parentElement || container;
|
||||||
|
const nextWidth = Math.max(1, Math.round(measurementTarget.clientWidth));
|
||||||
|
setViewportWidth((prev) => (prev === nextWidth ? prev : nextWidth));
|
||||||
|
};
|
||||||
|
|
||||||
|
updateWidth();
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
updateWidth();
|
||||||
|
});
|
||||||
|
observer.observe(container);
|
||||||
|
const parent = container.parentElement;
|
||||||
|
if (parent) {
|
||||||
|
observer.observe(parent);
|
||||||
|
}
|
||||||
|
const viewportNode = container.closest('.document-viewer__viewport') as HTMLElement | null;
|
||||||
|
if (viewportNode) {
|
||||||
|
observer.observe(viewportNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewportWidth <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const desiredWidth = Math.min(
|
||||||
|
MAX_RENDER_WIDTH,
|
||||||
|
Math.max(viewportWidth, MIN_RENDER_WIDTH),
|
||||||
|
);
|
||||||
|
setRenderWidth((current) => {
|
||||||
|
if (current === 0 || desiredWidth > current + RERENDER_DELTA) {
|
||||||
|
return desiredWidth;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
});
|
||||||
|
}, [viewportWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!src || renderWidth <= 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
let loadingTask: PDFDocumentLoadingTask | null = null;
|
||||||
|
let activePdf: PDFDocumentProxy | null = null;
|
||||||
|
const wasmUrl = ensureWasmUrl();
|
||||||
|
|
||||||
|
const disposeActivePdf = () => {
|
||||||
|
if (activePdf) {
|
||||||
|
void activePdf.destroy();
|
||||||
|
if (pdfRef.current === activePdf) {
|
||||||
|
pdfRef.current = null;
|
||||||
|
}
|
||||||
|
activePdf = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pdfRef.current) {
|
||||||
|
void pdfRef.current.destroy();
|
||||||
|
pdfRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPages([]);
|
||||||
|
setStatus('loading');
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
const renderDocument = async () => {
|
||||||
|
try {
|
||||||
|
loadingTask = getDocument({
|
||||||
|
url: src,
|
||||||
|
withCredentials: true,
|
||||||
|
wasmUrl,
|
||||||
|
});
|
||||||
|
const pdf = await loadingTask.promise;
|
||||||
|
if (cancelled) {
|
||||||
|
await pdf.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = window.devicePixelRatio || 1;
|
||||||
|
const descriptors: PageDescriptor[] = [];
|
||||||
|
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) {
|
||||||
|
if (cancelled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const page = await pdf.getPage(pageNumber);
|
||||||
|
if (cancelled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const baseViewport = page.getViewport({ scale: 1 });
|
||||||
|
const desiredScale = Math.max(0.5, renderWidth / baseViewport.width);
|
||||||
|
const viewport = page.getViewport({ scale: desiredScale });
|
||||||
|
descriptors.push({
|
||||||
|
number: pageNumber,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
scale: desiredScale,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
await pdf.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activePdf = pdf;
|
||||||
|
pdfRef.current = pdf;
|
||||||
|
setPixelRatio(ratio);
|
||||||
|
setPages(descriptors);
|
||||||
|
setStatus('ready');
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
console.error('Failed to prepare PDF preview', error);
|
||||||
|
setStatus('error');
|
||||||
|
setErrorMessage('Unable to render PDF preview.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderDocument().catch((error) => {
|
||||||
|
console.error('Unhandled PDF render error', error);
|
||||||
|
setStatus('error');
|
||||||
|
setErrorMessage('Unable to render PDF preview.');
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
loadingTask?.destroy();
|
||||||
|
disposeActivePdf();
|
||||||
|
};
|
||||||
|
}, [ensureWasmUrl, renderWidth, src]);
|
||||||
|
|
||||||
|
const viewerClasses = ['document-viewer__object', 'document-viewer__object--pdf', className]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
const statusMessage = status === 'error'
|
||||||
|
? (errorMessage || 'Unable to render PDF preview.')
|
||||||
|
: 'Preparing preview…';
|
||||||
|
const statusClasses = ['pdf-viewer__status', status === 'error' ? 'pdf-viewer__status--error' : null]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
const showStatus = status !== 'ready' || pages.length === 0;
|
||||||
|
|
||||||
|
const pageElements = useMemo(() => (
|
||||||
|
pages.map((page) => (
|
||||||
|
<Fragment key={`${page.number}-${Math.round(page.scale * 100)}-${Math.round(pixelRatio * 100)}`}>
|
||||||
|
<PdfPageCanvas
|
||||||
|
descriptor={page}
|
||||||
|
pdfRef={pdfRef}
|
||||||
|
pixelRatio={pixelRatio}
|
||||||
|
rootRef={containerRef}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
), [pages, pixelRatio]);
|
||||||
|
|
||||||
|
const handleCanvasStackClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (target.closest('.pdf-viewer__page-wrapper')) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={viewerClasses}>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="pdf-viewer__canvas-stack"
|
||||||
|
role="document"
|
||||||
|
aria-label={title || 'PDF document'}
|
||||||
|
onClick={handleCanvasStackClick}
|
||||||
|
>
|
||||||
|
{pageElements}
|
||||||
|
</div>
|
||||||
|
{showStatus ? (
|
||||||
|
<div className={statusClasses}>
|
||||||
|
<div className="document-viewer__message">
|
||||||
|
{statusMessage}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PdfPageCanvasProps {
|
||||||
|
descriptor: PageDescriptor;
|
||||||
|
pdfRef: MutableRefObject<PDFDocumentProxy | null>;
|
||||||
|
pixelRatio: number;
|
||||||
|
rootRef: RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PdfPageCanvas({
|
||||||
|
descriptor,
|
||||||
|
pdfRef,
|
||||||
|
pixelRatio,
|
||||||
|
rootRef,
|
||||||
|
}: PdfPageCanvasProps): JSX.Element {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const renderTaskRef = useRef<RenderTask | null>(null);
|
||||||
|
const hasRenderedRef = useRef(false);
|
||||||
|
const textLayerBuilderRef = useRef<TextLayerBuilder | null>(null);
|
||||||
|
|
||||||
|
const renderPage = useCallback(async () => {
|
||||||
|
if (renderTaskRef.current || hasRenderedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pdf = pdfRef.current;
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!pdf || !canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const page = await pdf.getPage(descriptor.number);
|
||||||
|
const viewport = page.getViewport({ scale: descriptor.scale });
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.width = viewport.width * pixelRatio;
|
||||||
|
canvas.height = viewport.height * pixelRatio;
|
||||||
|
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||||
|
const renderTask = page.render({ canvasContext: context, viewport, canvas });
|
||||||
|
renderTaskRef.current = renderTask;
|
||||||
|
await renderTask.promise;
|
||||||
|
hasRenderedRef.current = true;
|
||||||
|
|
||||||
|
const wrapper = wrapperRef.current;
|
||||||
|
if (wrapper) {
|
||||||
|
textLayerBuilderRef.current?.cancel();
|
||||||
|
const textLayerBuilder = new TextLayerBuilder({
|
||||||
|
pdfPage: page,
|
||||||
|
});
|
||||||
|
textLayerBuilderRef.current = textLayerBuilder;
|
||||||
|
wrapper.querySelector('.textLayer')?.remove();
|
||||||
|
wrapper.appendChild(textLayerBuilder.div);
|
||||||
|
await textLayerBuilder.render({ viewport });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to render PDF page', descriptor.number, error);
|
||||||
|
} finally {
|
||||||
|
renderTaskRef.current = null;
|
||||||
|
}
|
||||||
|
}, [descriptor.number, descriptor.scale, pdfRef, pixelRatio]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
canvas.style.width = '100%';
|
||||||
|
canvas.style.height = 'auto';
|
||||||
|
canvas.style.setProperty('aspect-ratio', `${descriptor.width} / ${descriptor.height}`);
|
||||||
|
const wrapperNode = wrapperRef.current;
|
||||||
|
return () => {
|
||||||
|
renderTaskRef.current?.cancel();
|
||||||
|
textLayerBuilderRef.current?.cancel();
|
||||||
|
wrapperNode?.querySelector('.textLayer')?.remove();
|
||||||
|
};
|
||||||
|
}, [descriptor.height, descriptor.width]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const node = canvasRef.current;
|
||||||
|
if (!node) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const root = rootRef.current || undefined;
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
void renderPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
root,
|
||||||
|
rootMargin: '200px 0px',
|
||||||
|
threshold: 0.1,
|
||||||
|
});
|
||||||
|
observer.observe(node);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [renderPage, rootRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={wrapperRef} className="pdf-viewer__page-wrapper">
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="pdf-viewer__page"
|
||||||
|
aria-label={`Page ${descriptor.number}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PdfViewer;
|
||||||
@@ -376,11 +376,13 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
grid-area: viewport;
|
grid-area: viewport;
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-viewer__object {
|
.document-viewer__object {
|
||||||
@@ -388,6 +390,13 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-viewer__object--pdf {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.document-viewer__object:not(.document-viewer__object--image) {
|
.document-viewer__object:not(.document-viewer__object--image) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -398,6 +407,7 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-viewer--stacked .document-viewer__object:not(.document-viewer__object--image) {
|
.document-viewer--stacked .document-viewer__object:not(.document-viewer__object--image) {
|
||||||
@@ -421,6 +431,66 @@
|
|||||||
background: var(--surface-subtle);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__canvas-stack {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__page-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: min(100%, 48rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__page {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
box-shadow: 0 1.25rem 2.5rem -1.25rem var(--shadow-medium);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__page-wrapper .textLayer {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__page-wrapper .textLayer span,
|
||||||
|
.pdf-viewer__page-wrapper .textLayer br {
|
||||||
|
position: absolute;
|
||||||
|
white-space: pre;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__page-wrapper .textLayer .endOfContent {
|
||||||
|
position: absolute;
|
||||||
|
inset: 100% 0 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__status {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: color-mix(in oklch, var(--surface) 85%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__status--error {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
.document-viewer__unsupported-filename {
|
.document-viewer__unsupported-filename {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 2rem;
|
|
||||||
z-index: 2000000;
|
z-index: 2000000;
|
||||||
cursor: zoom-out;
|
cursor: zoom-out;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -29,6 +28,13 @@
|
|||||||
z-index: 3000000;
|
z-index: 3000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-zoom__stage--pdf {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-zoom__image {
|
.preview-zoom__image {
|
||||||
max-width: 95vw;
|
max-width: 95vw;
|
||||||
max-height: 95vh;
|
max-height: 95vh;
|
||||||
@@ -42,7 +48,13 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 95vw;
|
max-width: 95vw;
|
||||||
max-height: 95vh;
|
max-height: 95vh;
|
||||||
box-shadow: 0 32px 120px var(--shadow-deep);
|
}
|
||||||
|
|
||||||
|
.preview-zoom__scroll--pdf {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-zoom__scroll:focus {
|
.preview-zoom__scroll:focus {
|
||||||
@@ -111,3 +123,28 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-zoom__pdf {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-zoom__pdf-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-zoom__stage .pdf-viewer__page-wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-zoom__stage .pdf-viewer__page-wrapper::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
box-shadow: 0 32px 120px var(--shadow-deep);
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*?url' {
|
||||||
|
const url: string;
|
||||||
|
export default url;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: './src/index.tsx',
|
entry: './src/index.tsx',
|
||||||
output: {
|
output: {
|
||||||
@@ -37,11 +38,26 @@ module.exports = {
|
|||||||
template: path.resolve(__dirname, 'src/index.html'),
|
template: path.resolve(__dirname, 'src/index.html'),
|
||||||
favicon: false,
|
favicon: false,
|
||||||
}),
|
}),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, 'node_modules/pdfjs-dist/wasm'),
|
||||||
|
to: 'pdfjs/wasm',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
devServer: {
|
devServer: {
|
||||||
static: {
|
static: [
|
||||||
|
{
|
||||||
directory: path.join(__dirname, 'public'),
|
directory: path.join(__dirname, 'public'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
directory: path.resolve(__dirname, 'node_modules/pdfjs-dist/wasm'),
|
||||||
|
publicPath: '/pdfjs/wasm',
|
||||||
|
watch: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
compress: true,
|
compress: true,
|
||||||
port: 5173,
|
port: 5173,
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user