234 lines
6.4 KiB
TypeScript
234 lines
6.4 KiB
TypeScript
import { useCallback, useMemo, useRef } from 'react';
|
|
import type { JSX } from 'react';
|
|
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
|
import { DownloadIcon } from '../ui/icons';
|
|
import PdfViewer from './PdfViewer';
|
|
import { AUDIO_EXTENSIONS, VIDEO_EXTENSIONS } from '../constants/preview';
|
|
|
|
import type { Document } from '../types/documents';
|
|
|
|
interface DocumentLink {
|
|
url?: string;
|
|
mimeType?: string;
|
|
filename?: string;
|
|
}
|
|
|
|
interface ContentTabConfig {
|
|
id?: string;
|
|
label?: string;
|
|
enabled?: boolean;
|
|
forceDisplay?: boolean;
|
|
loadContent?: (options?: { signal?: AbortSignal }) => unknown | Promise<unknown>;
|
|
loadingMessage?: string;
|
|
emptyMessage?: string;
|
|
unavailableMessage?: string;
|
|
errorMessage?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
type LayoutMode = 'split' | 'stacked' | (string & {});
|
|
|
|
interface DocumentViewerLayoutProps {
|
|
document?: Document | null;
|
|
documentLink?: DocumentLink | null;
|
|
summaryProps?: Record<string, unknown>;
|
|
metadataPayload?: unknown;
|
|
contentTabConfig?: ContentTabConfig | null;
|
|
resetKey?: string | null;
|
|
classNamePrefix?: string;
|
|
defaultTabId?: string;
|
|
infoPanelProps?: Record<string, unknown>;
|
|
previewLoadingMessage?: string;
|
|
layoutMode?: LayoutMode;
|
|
}
|
|
|
|
const getFileExtension = (filename?: string | null) => {
|
|
if (!filename) {
|
|
return '';
|
|
}
|
|
const match = filename.toLowerCase().match(/\.([a-z0-9]+)$/);
|
|
return match ? match[1] : '';
|
|
};
|
|
|
|
const DocumentViewerLayout = ({
|
|
document,
|
|
documentLink,
|
|
summaryProps = {},
|
|
metadataPayload,
|
|
contentTabConfig,
|
|
resetKey,
|
|
classNamePrefix = 'document-viewer',
|
|
defaultTabId = 'details',
|
|
infoPanelProps = {},
|
|
previewLoadingMessage = 'Preparing preview…',
|
|
layoutMode = 'split',
|
|
}: DocumentViewerLayoutProps): JSX.Element => {
|
|
const isStacked = layoutMode === 'stacked';
|
|
const viewportRef = useRef<HTMLDivElement>(null);
|
|
|
|
const previewContent = useMemo(() => {
|
|
if (!document || !documentLink?.url) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedMimeType = (documentLink.mimeType
|
|
|| document.mime_type
|
|
|| '')
|
|
.toLowerCase();
|
|
const normalizedFilename = documentLink.filename
|
|
|| document.filename
|
|
|| document.original_name
|
|
|| '';
|
|
const fileExtension = getFileExtension(normalizedFilename);
|
|
const isImage = normalizedMimeType.startsWith('image/');
|
|
const isPdf = normalizedMimeType === 'application/pdf'
|
|
|| normalizedMimeType === 'application/x-pdf';
|
|
const isAudio = normalizedMimeType.startsWith('audio/')
|
|
|| AUDIO_EXTENSIONS.has(fileExtension);
|
|
const isVideo = normalizedMimeType.startsWith('video/')
|
|
|| VIDEO_EXTENSIONS.has(fileExtension);
|
|
const mediaLabel = document.title
|
|
|| normalizedFilename
|
|
|| 'Document preview';
|
|
|
|
if (isImage) {
|
|
return (
|
|
<img
|
|
src={documentLink.url}
|
|
alt={`Preview of ${document.title}`}
|
|
className="document-viewer__object document-viewer__object--image"
|
|
draggable={false}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (isPdf) {
|
|
const documentTitle = document.title
|
|
|| document.filename
|
|
|| document.original_name;
|
|
return (
|
|
<PdfViewer
|
|
src={documentLink.url}
|
|
title={documentTitle ? `Preview of ${documentTitle}` : undefined}
|
|
viewportRef={viewportRef}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (isAudio) {
|
|
return (
|
|
<audio
|
|
className="document-viewer__object document-viewer__object--audio"
|
|
controls
|
|
preload="metadata"
|
|
src={documentLink.url}
|
|
aria-label={`Audio preview of ${mediaLabel}`}
|
|
>
|
|
</audio>
|
|
);
|
|
}
|
|
|
|
if (isVideo) {
|
|
return (
|
|
<video
|
|
className="document-viewer__object document-viewer__object--video"
|
|
controls
|
|
preload="metadata"
|
|
src={documentLink.url}
|
|
aria-label={`Video preview of ${mediaLabel}`}
|
|
>
|
|
</video>
|
|
);
|
|
}
|
|
|
|
const displayMimeType = document.mime_type || documentLink.mimeType || 'this file type';
|
|
const displayFilename = documentLink.filename
|
|
|| document.filename
|
|
|| document.original_name
|
|
|| 'download';
|
|
|
|
return (
|
|
<div className="document-viewer__unsupported">
|
|
<div className="document-viewer__unsupported-message">
|
|
Preview is not available for {displayMimeType} files.
|
|
</div>
|
|
<div className="document-viewer__unsupported-filename">{displayFilename}</div>
|
|
<a
|
|
className="button-link document-viewer__unsupported-download"
|
|
href={documentLink.url}
|
|
download={displayFilename}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<DownloadIcon />
|
|
<span>Download</span>
|
|
</a>
|
|
</div>
|
|
);
|
|
}, [document, documentLink, viewportRef]);
|
|
|
|
const renderViewportPane = useCallback(() => (
|
|
<div className="document-viewer__viewport" ref={viewportRef}>
|
|
{!documentLink?.url ? (
|
|
<div className="document-viewer__message">{previewLoadingMessage}</div>
|
|
) : (
|
|
previewContent
|
|
)}
|
|
</div>
|
|
), [previewContent, documentLink?.url, previewLoadingMessage, viewportRef]);
|
|
|
|
const viewportPane = renderViewportPane();
|
|
|
|
const stackedLeadingTabs = useMemo(() => (
|
|
isStacked
|
|
? [
|
|
{
|
|
id: 'preview',
|
|
label: 'Preview',
|
|
render: () => renderViewportPane(),
|
|
},
|
|
]
|
|
: []
|
|
), [isStacked, renderViewportPane]);
|
|
|
|
const resolvedDefaultTabId = isStacked ? 'preview' : defaultTabId;
|
|
const summaryPlacement = 'tabs';
|
|
const tabsPlacement = 'bottom';
|
|
const summaryLayout = 'compact';
|
|
|
|
const detailsPane = (
|
|
<div className="document-viewer__details-pane">
|
|
<div className="document-viewer__details">
|
|
<DocumentInfoPanel
|
|
document={document}
|
|
summaryProps={summaryProps}
|
|
metadataPayload={metadataPayload}
|
|
contentConfig={contentTabConfig}
|
|
defaultTabId={resolvedDefaultTabId}
|
|
classNamePrefix={classNamePrefix}
|
|
hideTabNavWhenSingle={false}
|
|
resetKey={resetKey || document?.id}
|
|
summaryPlacement={summaryPlacement}
|
|
summaryLayout={summaryLayout}
|
|
leadingTabs={stackedLeadingTabs}
|
|
tabsPlacement={tabsPlacement}
|
|
{...infoPanelProps}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (isStacked) {
|
|
return detailsPane;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{detailsPane}
|
|
{viewportPane}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default DocumentViewerLayout;
|