pdfjs
This commit is contained in:
Generated
+284
@@ -11,6 +11,7 @@
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"axios": "^1.13.2",
|
||||
"pdfjs-dist": "^5.4.394",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5"
|
||||
@@ -24,6 +25,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"babel-loader": "^10.0.0",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"eslint": "^9.39.1",
|
||||
@@ -2307,6 +2309,191 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -4524,6 +4711,43 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.46.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
|
||||
@@ -8159,6 +8383,18 @@
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -9905,6 +10141,54 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"axios": "^1.13.2",
|
||||
"pdfjs-dist": "^5.4.394",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5"
|
||||
@@ -26,6 +27,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"babel-loader": "^10.0.0",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"eslint": "^9.39.1",
|
||||
|
||||
@@ -60,7 +60,7 @@ interface UseDocumentPreviewArgs {
|
||||
interface UseDocumentPreviewResult {
|
||||
previewEntries: Map<DocumentId, PreviewEntry>;
|
||||
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>;
|
||||
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => 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> => {
|
||||
if (!documentId) return null;
|
||||
|
||||
@@ -231,7 +231,7 @@ const useDocumentPreview = ({
|
||||
fallbackFolderId === 'root' ? '/documents' : `/documents/folder/${fallbackFolderId}`;
|
||||
}
|
||||
|
||||
await ensurePreviewUrl(documentId, { force: false });
|
||||
await ensureDownloadUrl(documentId, { force: false });
|
||||
setActivePreviewId(documentId);
|
||||
return doc;
|
||||
},
|
||||
@@ -239,7 +239,7 @@ const useDocumentPreview = ({
|
||||
searchResults,
|
||||
documents,
|
||||
assetManager,
|
||||
ensurePreviewUrl,
|
||||
ensureDownloadUrl,
|
||||
setActivePreviewId,
|
||||
api,
|
||||
cachePreviewDocument,
|
||||
@@ -336,7 +336,7 @@ const useDocumentPreview = ({
|
||||
return {
|
||||
previewEntries,
|
||||
previewDocuments,
|
||||
ensurePreviewUrl,
|
||||
ensureDownloadUrl,
|
||||
ensurePreviewData,
|
||||
openDocumentPreview,
|
||||
closeDocumentPreview,
|
||||
|
||||
@@ -62,7 +62,7 @@ const DesktopPreviewCard = ({
|
||||
}: DesktopPreviewCardProps): JSX.Element => {
|
||||
const navigator = useAssetNavigator({
|
||||
document: doc,
|
||||
assetType: 'preview',
|
||||
assetType: 'thumbnail',
|
||||
ensureAssetUrl: shouldLoad ? ensureAssetUrl : null,
|
||||
getAsset: getDocumentAsset,
|
||||
prefetch,
|
||||
|
||||
@@ -8,6 +8,7 @@ import React, {
|
||||
useSyncExternalStore,
|
||||
} from 'react';
|
||||
import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||
import type { EnsureAssetUrl, GetAsset } from '../asset_manager';
|
||||
import { formatTransform } from './math';
|
||||
import useDocumentDrag from './useDocumentDrag';
|
||||
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
||||
@@ -31,6 +32,8 @@ import '../styles/workspace/workspace-cards.css';
|
||||
type Identifier = string | number;
|
||||
|
||||
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 {
|
||||
id?: Identifier | null;
|
||||
@@ -49,6 +52,7 @@ interface NavigatorSnapshot {
|
||||
ordinal?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
contentType?: string | null;
|
||||
}
|
||||
|
||||
type OverlayOriginHint = {
|
||||
@@ -124,8 +128,8 @@ interface DesktopWorkspaceProps {
|
||||
onDocumentStackSelect?: (docIds: Identifier[]) => void;
|
||||
onPromoteSelection?: (...args: unknown[]) => void;
|
||||
onAssignTagToDocument?: (...args: unknown[]) => void;
|
||||
ensureAssetUrl?: (...args: unknown[]) => Promise<unknown> | unknown;
|
||||
getDocumentAsset?: (...args: unknown[]) => unknown;
|
||||
ensureAssetUrl?: EnsureAssetUrl;
|
||||
getDocumentAsset?: GetAsset;
|
||||
activeTagIds?: Array<Identifier | null>;
|
||||
selectedDocumentIds?: Identifier[];
|
||||
onClearSelection?: () => void;
|
||||
@@ -193,6 +197,8 @@ interface DesktopWorkspaceViewProps {
|
||||
|
||||
const DEBUG_DRAG = false;
|
||||
const DEBUG_FOCUS = false;
|
||||
const defaultGetDocumentAsset: GetAsset = () => null;
|
||||
|
||||
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
documents = [],
|
||||
searchResults = null,
|
||||
@@ -202,7 +208,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
onPromoteSelection = null,
|
||||
onAssignTagToDocument = null,
|
||||
ensureAssetUrl = null,
|
||||
getDocumentAsset = () => null,
|
||||
getDocumentAsset = defaultGetDocumentAsset,
|
||||
activeTagIds = [],
|
||||
selectedDocumentIds = [],
|
||||
onClearSelection = null,
|
||||
@@ -210,6 +216,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
onCloseDetailPanel = null,
|
||||
tenantId = null,
|
||||
viewId = 'default',
|
||||
previewEntries,
|
||||
ensureDownloadUrl,
|
||||
}) => {
|
||||
const items = useMemo<DeskDocument[]>(
|
||||
() => (searchResults ? searchResults : documents),
|
||||
@@ -217,6 +225,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
);
|
||||
|
||||
const allowLayoutPersistence = Boolean(tenantId && viewId && viewId.startsWith('folder:'));
|
||||
const previewEntryMap = previewEntries instanceof Map ? previewEntries : null;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const itemRefs = useRef<Map<string, HTMLDivElement | null>>(new Map());
|
||||
@@ -226,9 +235,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
const [overlayOriginTransform, setOverlayOriginTransform] = useState<OverlayOriginTransform | null>(
|
||||
null,
|
||||
);
|
||||
const [previewSnapshots, setPreviewSnapshots] = useState<Map<string, NavigatorSnapshot>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [overlaySource, setOverlaySource] = useState<OverlaySource | null>(null);
|
||||
const [, setPreviewSnapshots] = useState<Map<string, NavigatorSnapshot>>(() => new Map());
|
||||
const [docSizeVersion, setDocSizeVersion] = useState(0);
|
||||
const docSizeMapRef = useRef<Map<string, DocumentSizeInfo>>(new Map());
|
||||
const ensureDocumentSize = useCallback((doc: DeskDocument | null): DocumentSizeInfo | null => {
|
||||
@@ -444,7 +452,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
resolveDocumentAssetUrl(doc, 'preview', {
|
||||
resolveDocumentAssetUrl(doc, 'thumbnail', {
|
||||
ensureAssetUrl,
|
||||
getAsset: getDocumentAsset,
|
||||
});
|
||||
@@ -546,30 +554,77 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
}
|
||||
}, [items, previewMetadata]);
|
||||
|
||||
const overlayDisplay = useMemo<OverlayDisplay | null>(() => {
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!overlayDocId) {
|
||||
return null;
|
||||
setOverlaySource(null);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
const snapshot = previewSnapshots.get(overlayDocId);
|
||||
if (!snapshot || !snapshot.url) {
|
||||
return null;
|
||||
|
||||
const doc = documentLookup.get(overlayDocId) || null;
|
||||
const docIdentifier = doc?.id ?? null;
|
||||
if (!docIdentifier || !doc) {
|
||||
setOverlaySource(null);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
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,
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
}, [overlayDocId, previewSnapshots, documentLookup]);
|
||||
|
||||
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(() => {
|
||||
setOverlayDocId(null);
|
||||
setOverlayOriginRect(null);
|
||||
setOverlayOriginTransform(null);
|
||||
setOverlaySource(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -611,24 +666,32 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
}
|
||||
}, [draggingId, items, setDraggingId]);
|
||||
|
||||
const overlayDisplay = useMemo<OverlayDisplay | null>(() => {
|
||||
if (!overlaySource) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...overlaySource,
|
||||
canGoPrev: false,
|
||||
canGoNext: false,
|
||||
};
|
||||
}, [overlaySource]);
|
||||
|
||||
const openOverlayForDoc = useCallback(
|
||||
(docId: Identifier | null, originInfo: OverlayOriginHint | null = null) => {
|
||||
if (!docId) {
|
||||
return;
|
||||
}
|
||||
const docKey = String(docId);
|
||||
const snapshot = previewSnapshots.get(docKey);
|
||||
if (!snapshot || !snapshot.url) {
|
||||
return;
|
||||
}
|
||||
const container = itemRefs.current.get(docKey);
|
||||
const imageNode = container
|
||||
? container.querySelector<HTMLImageElement>('.desk-item__card img')
|
||||
: null;
|
||||
if (!container || !imageNode) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const imageNode = container.querySelector<HTMLImageElement>('.desk-item__card img');
|
||||
const rect = (imageNode || container).getBoundingClientRect();
|
||||
if (!rect) {
|
||||
return;
|
||||
}
|
||||
const rect = imageNode.getBoundingClientRect();
|
||||
let originTransform = null;
|
||||
if (originInfo) {
|
||||
const { rotation = 0, scale = 1, width: originWidth, height: originHeight } = originInfo;
|
||||
@@ -666,7 +729,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
},
|
||||
[
|
||||
bringToFront,
|
||||
previewSnapshots,
|
||||
itemRefs,
|
||||
setOverlayOriginTransform,
|
||||
ensureDocumentSize,
|
||||
|
||||
@@ -61,6 +61,8 @@ interface UseDeskWorkspacePropsArgs {
|
||||
searchQuery?: string;
|
||||
activeCorrespondentFilters?: Identifier[];
|
||||
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 = ({
|
||||
@@ -108,6 +110,8 @@ const useDeskWorkspaceProps = ({
|
||||
searchQuery = '',
|
||||
activeCorrespondentFilters = [],
|
||||
selectedFolder,
|
||||
previewEntries,
|
||||
ensureDownloadUrl,
|
||||
}: UseDeskWorkspacePropsArgs) => {
|
||||
const handleDeskDocumentStackSelect: DeskDocumentStackSelectHandler = useCallback(
|
||||
(docIds) => {
|
||||
@@ -200,6 +204,8 @@ const useDeskWorkspaceProps = ({
|
||||
onMoveDocumentsToFolder: moveDocumentsToFolder,
|
||||
searchIncludeDescendants,
|
||||
onToggleSearchIncludeDescendants: toggleSearchIncludeDescendants,
|
||||
previewEntries,
|
||||
ensureDownloadUrl,
|
||||
}),
|
||||
[
|
||||
documents,
|
||||
@@ -240,6 +246,8 @@ const useDeskWorkspaceProps = ({
|
||||
moveDocumentsToFolder,
|
||||
searchIncludeDescendants,
|
||||
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 { ArrowLeftIcon, ArrowRightIcon } from '../ui/icons';
|
||||
import { clamp } from '../utils/math';
|
||||
import PdfViewer from '../preview/PdfViewer';
|
||||
|
||||
type PreviewNavigator = {
|
||||
url: string;
|
||||
alt?: string;
|
||||
contentType?: string | null;
|
||||
pageNumber?: number | null;
|
||||
canGoPrev?: boolean;
|
||||
canGoNext?: boolean;
|
||||
goPrev?: () => void;
|
||||
@@ -20,6 +23,22 @@ interface PreviewZoomOverlayProps {
|
||||
|
||||
type NaturalSize = { width: number | null; height: 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 = () => {};
|
||||
|
||||
@@ -35,7 +54,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
const [isBackdropVisible, setBackdropVisible] = useState(false);
|
||||
const [displaySnapshot, setDisplaySnapshot] = useState<PreviewNavigator | 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 previouslyFocusedRef = useRef<HTMLElement | null>(null);
|
||||
const visibilityTimerRef = useRef<number | null>(null);
|
||||
@@ -47,6 +66,10 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
}
|
||||
}, [display]);
|
||||
|
||||
const activeDisplay = open && display?.url ? display : displaySnapshot;
|
||||
const displayKind = useMemo(() => determineDisplayKind(activeDisplay), [activeDisplay]);
|
||||
const isPdfDisplay = displayKind === 'pdf';
|
||||
|
||||
useEffect(() => {
|
||||
if (visibilityTimerRef.current) {
|
||||
clearTimeout(visibilityTimerRef.current);
|
||||
@@ -94,6 +117,9 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
}, [isPdfDisplay, open, activeDisplay?.url]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsNativeScale(false);
|
||||
setNaturalSize({ width: null, height: null });
|
||||
@@ -103,7 +129,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
scrollEl.scrollLeft = 0;
|
||||
scrollEl.scrollTop = 0;
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, activeDisplay?.url, displayKind]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -112,13 +138,13 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
}
|
||||
|
||||
const scrollEl = scrollRef.current;
|
||||
const imageEl = imageRef.current;
|
||||
if (!scrollEl || !imageEl) {
|
||||
const mediaEl = mediaRef.current;
|
||||
if (!scrollEl || !mediaEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageWidth = imageEl.naturalWidth || imageEl.clientWidth;
|
||||
const imageHeight = imageEl.naturalHeight || imageEl.clientHeight;
|
||||
const imageWidth = naturalSize.width || mediaEl.clientWidth;
|
||||
const imageHeight = naturalSize.height || mediaEl.clientHeight;
|
||||
if (!(imageWidth > 0 && imageHeight > 0)) {
|
||||
return;
|
||||
}
|
||||
@@ -145,8 +171,6 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
previouslyFocusedRef.current = active instanceof HTMLElement ? active : null;
|
||||
}, [open]);
|
||||
|
||||
const activeDisplay = open && display?.url ? display : displaySnapshot;
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeDisplay?.url) {
|
||||
return;
|
||||
@@ -222,10 +246,13 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
};
|
||||
|
||||
const toggleZoomAtPoint = (clientX: number, clientY: number) => {
|
||||
const img = imageRef.current;
|
||||
if (isPdfDisplay) {
|
||||
return;
|
||||
}
|
||||
const media = mediaRef.current;
|
||||
setIsNativeScale((current) => {
|
||||
if (!current && img) {
|
||||
const rect = img.getBoundingClientRect();
|
||||
if (!current && media) {
|
||||
const rect = media.getBoundingClientRect();
|
||||
const xRatio = rect.width > 0 ? (clientX - rect.left) / rect.width : 0.5;
|
||||
const yRatio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0.5;
|
||||
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();
|
||||
if (isPdfDisplay) {
|
||||
return;
|
||||
}
|
||||
toggleZoomAtPoint(event.clientX ?? 0, event.clientY ?? 0);
|
||||
};
|
||||
|
||||
if (!renderBackdrop || !activeDisplay?.url) {
|
||||
return null;
|
||||
}
|
||||
const shouldRender = renderBackdrop && Boolean(activeDisplay?.url);
|
||||
|
||||
const effectiveDisplay = activeDisplay;
|
||||
const navVisible = Boolean(effectiveDisplay?.canGoPrev || effectiveDisplay?.canGoNext);
|
||||
const stageClassName = 'preview-zoom__stage';
|
||||
const stageClassName = isPdfDisplay
|
||||
? 'preview-zoom__stage preview-zoom__stage--pdf'
|
||||
: 'preview-zoom__stage';
|
||||
|
||||
const containerClassName = [
|
||||
'preview-zoom__scroll',
|
||||
isNativeScale ? 'preview-zoom__scroll--native' : '',
|
||||
isNativeScale ? 'preview-zoom__scroll--native' : null,
|
||||
isPdfDisplay ? 'preview-zoom__scroll--pdf' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
@@ -266,7 +295,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
const imageStyle: CSSProperties = isNativeScale
|
||||
const contentStyle: CSSProperties = isNativeScale
|
||||
? {
|
||||
cursor: 'zoom-out',
|
||||
width: naturalSize.width ? `${naturalSize.width}px` : 'auto',
|
||||
@@ -282,6 +311,13 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
touchAction: 'manipulation',
|
||||
};
|
||||
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const effectiveDisplay = activeDisplay;
|
||||
const navVisible = Boolean(effectiveDisplay?.canGoPrev || effectiveDisplay?.canGoNext);
|
||||
|
||||
return createPortal(
|
||||
(
|
||||
<div
|
||||
@@ -290,29 +326,48 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
aria-modal="true"
|
||||
aria-label="Enlarged document preview"
|
||||
onClick={onClose}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div className={stageClassName} onKeyDown={handleKeyDown}>
|
||||
<div
|
||||
className={stageClassName}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={containerClassName}
|
||||
ref={scrollRef}
|
||||
tabIndex={-1}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<img
|
||||
src={effectiveDisplay.url}
|
||||
alt={effectiveDisplay.alt || 'Document preview'}
|
||||
className="preview-zoom__image"
|
||||
ref={imageRef}
|
||||
draggable={false}
|
||||
onLoad={(event) => {
|
||||
setNaturalSize({
|
||||
width: event.currentTarget.naturalWidth || null,
|
||||
height: event.currentTarget.naturalHeight || null,
|
||||
});
|
||||
}}
|
||||
onClick={handleImageClick}
|
||||
style={imageStyle}
|
||||
/>
|
||||
{isPdfDisplay ? (
|
||||
<div className="preview-zoom__pdf">
|
||||
<PdfViewer
|
||||
src={effectiveDisplay.url}
|
||||
title={effectiveDisplay.alt || 'Document preview'}
|
||||
className="preview-zoom__pdf-viewer"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={effectiveDisplay.url}
|
||||
alt={effectiveDisplay.alt || 'Document preview'}
|
||||
className="preview-zoom__image"
|
||||
ref={(node) => {
|
||||
mediaRef.current = node;
|
||||
}}
|
||||
draggable={false}
|
||||
onLoad={(event) => {
|
||||
setNaturalSize({
|
||||
width: event.currentTarget.naturalWidth || null,
|
||||
height: event.currentTarget.naturalHeight || null,
|
||||
});
|
||||
}}
|
||||
onClick={handleContentClick}
|
||||
style={contentStyle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{navVisible ? (
|
||||
<div className="preview-zoom__nav">
|
||||
|
||||
@@ -2,6 +2,11 @@ import { useMemo } from 'react';
|
||||
|
||||
type Identifier = string | number;
|
||||
|
||||
interface PreviewEntryLike {
|
||||
url?: string | null;
|
||||
contentType?: string | null;
|
||||
}
|
||||
|
||||
export interface Breadcrumb {
|
||||
id?: Identifier;
|
||||
name?: string;
|
||||
@@ -65,6 +70,8 @@ export interface UseDocumentsPanelPropsArgs {
|
||||
handleBulkSelectionReanalyze?: (...args: unknown[]) => void;
|
||||
folderOptions?: unknown[];
|
||||
moveDocumentsToFolder?: (...args: unknown[]) => void;
|
||||
previewEntries?: Map<Identifier, PreviewEntryLike>;
|
||||
ensureDownloadUrl?: (documentId: Identifier, options?: { force?: boolean }) => Promise<PreviewEntryLike | null>;
|
||||
}
|
||||
|
||||
export type DocumentsPanelProps = ReturnType<typeof useDocumentsPanelProps>;
|
||||
@@ -93,8 +100,8 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
||||
searchLoading,
|
||||
tagLookupById,
|
||||
activeCorrespondentFilters,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
toggleTagFilter,
|
||||
toggleCorrespondentFilter,
|
||||
handleDocumentTagDrop,
|
||||
@@ -119,6 +126,8 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
||||
handleBulkSelectionReanalyze,
|
||||
folderOptions,
|
||||
moveDocumentsToFolder,
|
||||
previewEntries,
|
||||
ensureDownloadUrl,
|
||||
} = props;
|
||||
|
||||
return useMemo(
|
||||
@@ -173,6 +182,8 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
||||
onBulkReanalyze: handleBulkSelectionReanalyze,
|
||||
folderOptions,
|
||||
onMoveDocumentsToFolder: moveDocumentsToFolder,
|
||||
previewEntries,
|
||||
ensureDownloadUrl,
|
||||
}),
|
||||
[
|
||||
activeCorrespondentFilters,
|
||||
@@ -223,6 +234,8 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => {
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
folderOptions,
|
||||
previewEntries,
|
||||
ensureDownloadUrl,
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import DocumentsGrid from '../DocumentsGrid';
|
||||
import DocumentsList from '../DocumentsList';
|
||||
import { isTagTransferEvent } from '../tagTransfer';
|
||||
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
|
||||
import { useAssetNavigator } from '../../hooks/useAssetNavigator';
|
||||
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
|
||||
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
|
||||
|
||||
@@ -22,6 +21,8 @@ interface DocumentsPanelProps {
|
||||
|
||||
const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
|
||||
|
||||
export type PreviewEntryLike = { url?: string | null; contentType?: string | null };
|
||||
|
||||
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
currentFolderName,
|
||||
breadcrumbs,
|
||||
@@ -55,6 +56,8 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
viewMode = 'list',
|
||||
onViewModeChange,
|
||||
showHeader = true,
|
||||
previewEntries,
|
||||
ensureDownloadUrl,
|
||||
}) => {
|
||||
const {
|
||||
selectedEntries,
|
||||
@@ -65,6 +68,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
} = useWorkspaceSelectionContext();
|
||||
const showingSearchResults = searchResults !== null;
|
||||
const rows = showingSearchResults ? searchResults : documents;
|
||||
const previewEntryMap = previewEntries instanceof Map ? previewEntries : null;
|
||||
|
||||
const currentFolderId = useMemo(() => {
|
||||
if (showingSearchResults) {
|
||||
@@ -138,7 +142,10 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
const isGridView = viewMode === 'grid';
|
||||
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(() => {
|
||||
if (!previewDocId) {
|
||||
@@ -153,38 +160,69 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
}
|
||||
}, [previewDocId, previewDoc]);
|
||||
|
||||
const previewNavigator = useAssetNavigator({
|
||||
document: previewDoc,
|
||||
assetType: 'preview',
|
||||
ensureAssetUrl,
|
||||
getAsset: getDocumentAsset,
|
||||
prefetch: 3,
|
||||
});
|
||||
const [previewZoomSource, setPreviewZoomSource] = useState<ZoomSource | null>(null);
|
||||
const zoomDisplay = previewZoomSource;
|
||||
|
||||
const {
|
||||
currentUrl: previewUrl,
|
||||
canGoPrev: previewCanGoPrev,
|
||||
canGoNext: previewCanGoNext,
|
||||
goPrev: previewGoPrev,
|
||||
goNext: previewGoNext,
|
||||
} = previewNavigator;
|
||||
|
||||
const previewDisplay = useMemo(() => {
|
||||
if (!previewDoc || !previewUrl) {
|
||||
return null;
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!previewDocId || !previewDoc) {
|
||||
setPreviewZoomSource(null);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
return {
|
||||
url: previewUrl,
|
||||
alt: previewDoc.title,
|
||||
canGoPrev: Boolean(previewCanGoPrev),
|
||||
canGoNext: Boolean(previewCanGoNext),
|
||||
goPrev: previewGoPrev,
|
||||
goNext: previewGoNext,
|
||||
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,
|
||||
});
|
||||
};
|
||||
}, [previewDoc, previewUrl, previewCanGoPrev, previewCanGoNext, previewGoPrev, previewGoNext]);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [previewDocId, previewDoc, previewEntryMap, ensureDownloadUrl]);
|
||||
|
||||
const closePreviewOverlay = useCallback(() => {
|
||||
setPreviewDocId(null);
|
||||
setPreviewZoomSource(null);
|
||||
}, []);
|
||||
|
||||
const handleDocumentPreviewZoom = useCallback(
|
||||
@@ -192,13 +230,12 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
if (!doc || !doc.id) {
|
||||
return;
|
||||
}
|
||||
const previewAsset = getDocumentAsset(doc, 'preview');
|
||||
if (!previewAsset) {
|
||||
if (!ensureDownloadUrl && !(previewEntryMap?.get(doc.id)?.url)) {
|
||||
return;
|
||||
}
|
||||
setPreviewDocId(doc.id);
|
||||
},
|
||||
[getDocumentAsset],
|
||||
[ensureDownloadUrl, previewEntryMap],
|
||||
);
|
||||
|
||||
const handleDocumentActivate = useCallback(
|
||||
@@ -724,8 +761,8 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
)}
|
||||
</section>
|
||||
<PreviewZoomOverlay
|
||||
open={Boolean(previewDocId)}
|
||||
display={previewDisplay}
|
||||
open={Boolean(zoomDisplay?.url)}
|
||||
display={zoomDisplay}
|
||||
onClose={closePreviewOverlay}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -382,6 +382,7 @@ const useDocumentsWorkspace = ({
|
||||
previewEntries,
|
||||
previewDocuments,
|
||||
ensurePreviewData,
|
||||
ensureDownloadUrl,
|
||||
openDocumentPreview,
|
||||
closeDocumentPreview,
|
||||
resetPreviewState,
|
||||
@@ -1452,6 +1453,8 @@ const useDocumentsWorkspace = ({
|
||||
folderOptions,
|
||||
moveDocumentsToFolder,
|
||||
selectFolder,
|
||||
previewEntries,
|
||||
ensureDownloadUrl,
|
||||
});
|
||||
|
||||
const sidebarProps = useSidebarProps({
|
||||
@@ -1539,6 +1542,8 @@ const useDocumentsWorkspace = ({
|
||||
searchQuery,
|
||||
activeCorrespondentFilters,
|
||||
selectedFolder,
|
||||
previewEntries,
|
||||
ensureDownloadUrl,
|
||||
});
|
||||
|
||||
const contextValue = useMemo(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
||||
import { DownloadIcon } from '../ui/icons';
|
||||
import PdfViewer from './PdfViewer';
|
||||
|
||||
interface DocumentLike {
|
||||
id?: string | number;
|
||||
@@ -87,11 +88,13 @@ const DocumentViewerLayout = ({
|
||||
}
|
||||
|
||||
if (isPdf) {
|
||||
const documentTitle = document.title
|
||||
|| document.filename
|
||||
|| document.original_name;
|
||||
return (
|
||||
<iframe
|
||||
<PdfViewer
|
||||
src={previewEntry.url}
|
||||
title={`Preview of ${document.title}`}
|
||||
className="document-viewer__object"
|
||||
title={documentTitle ? `Preview of ${documentTitle}` : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import { createDocumentActionState } from '../documents/documentActions';
|
||||
import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||
import PanelHeader from '../ui/PanelHeader';
|
||||
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
||||
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
||||
import DocumentViewerLayout from './DocumentViewerLayout';
|
||||
import useViewerLayoutMode from './useViewerLayoutMode';
|
||||
import { usePanelResizeBindings } from '../app/PanelManagerContext';
|
||||
@@ -33,8 +32,13 @@ import { usePanelResizeBindings } from '../app/PanelManagerContext';
|
||||
interface DocumentLike {
|
||||
id?: string | number;
|
||||
title?: string;
|
||||
content_type?: string | null;
|
||||
issued_at?: string | null;
|
||||
correspondents?: Array<{ id?: string | number; name?: string }>;
|
||||
current_version?: {
|
||||
version_number?: number;
|
||||
version?: { content_type?: string | null } | null;
|
||||
} | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -245,18 +249,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
);
|
||||
|
||||
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(() => {
|
||||
if (!previewEntry?.url) {
|
||||
@@ -360,34 +352,18 @@ const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(n
|
||||
}, [breadcrumbs, handleBreadcrumbNavigate]);
|
||||
|
||||
const zoomDisplay = useMemo(() => {
|
||||
if (navigatorUrl && document) {
|
||||
return {
|
||||
url: navigatorUrl,
|
||||
alt: document.title,
|
||||
canGoPrev: navigatorCanGoPrev,
|
||||
canGoNext: navigatorCanGoNext,
|
||||
goPrev: navigatorCanGoPrev ? navigatorGoPrev : undefined,
|
||||
goNext: navigatorCanGoNext ? navigatorGoNext : undefined,
|
||||
};
|
||||
if (!previewEntry?.url || !document) {
|
||||
return null;
|
||||
}
|
||||
if (previewEntry?.url && document) {
|
||||
return {
|
||||
url: previewEntry.url,
|
||||
alt: document.title,
|
||||
canGoPrev: false,
|
||||
canGoNext: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
navigatorUrl,
|
||||
navigatorCanGoPrev,
|
||||
navigatorCanGoNext,
|
||||
navigatorGoPrev,
|
||||
navigatorGoNext,
|
||||
previewEntry?.url,
|
||||
document,
|
||||
]);
|
||||
const docContentType = document.content_type;
|
||||
const versionContentType = document.current_version?.version?.content_type;
|
||||
const normalizedContentType = previewEntry.contentType || docContentType || versionContentType || null;
|
||||
return {
|
||||
url: previewEntry.url,
|
||||
alt: document.title,
|
||||
contentType: normalizedContentType || undefined,
|
||||
};
|
||||
}, [previewEntry?.url, previewEntry?.contentType, document]);
|
||||
|
||||
const headerActions = createDocumentViewerHeaderActions({
|
||||
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;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
max-height: 100%;
|
||||
grid-area: viewport;
|
||||
background: var(--surface-subtle);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.document-viewer__object {
|
||||
@@ -388,6 +390,13 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.document-viewer__object--pdf {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.document-viewer__object:not(.document-viewer__object--image) {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -398,6 +407,7 @@
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.document-viewer--stacked .document-viewer__object:not(.document-viewer__object--image) {
|
||||
@@ -421,6 +431,66 @@
|
||||
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 {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
z-index: 2000000;
|
||||
cursor: zoom-out;
|
||||
opacity: 0;
|
||||
@@ -29,6 +28,13 @@
|
||||
z-index: 3000000;
|
||||
}
|
||||
|
||||
.preview-zoom__stage--pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.preview-zoom__image {
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
@@ -42,7 +48,13 @@
|
||||
justify-content: center;
|
||||
max-width: 95vw;
|
||||
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 {
|
||||
@@ -111,3 +123,28 @@
|
||||
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 HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
module.exports = {
|
||||
entry: './src/index.tsx',
|
||||
output: {
|
||||
@@ -37,11 +38,26 @@ module.exports = {
|
||||
template: path.resolve(__dirname, 'src/index.html'),
|
||||
favicon: false,
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.resolve(__dirname, 'node_modules/pdfjs-dist/wasm'),
|
||||
to: 'pdfjs/wasm',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, 'public'),
|
||||
},
|
||||
static: [
|
||||
{
|
||||
directory: path.join(__dirname, 'public'),
|
||||
},
|
||||
{
|
||||
directory: path.resolve(__dirname, 'node_modules/pdfjs-dist/wasm'),
|
||||
publicPath: '/pdfjs/wasm',
|
||||
watch: false,
|
||||
},
|
||||
],
|
||||
compress: true,
|
||||
port: 5173,
|
||||
historyApiFallback: true,
|
||||
|
||||
Reference in New Issue
Block a user