Skip to content

Commit d299b37

Browse files
committed
feat(docviewer): add docx support
1 parent 459de3f commit d299b37

File tree

10 files changed

+438
-42
lines changed

10 files changed

+438
-42
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { createElement, Fragment, useCallback, useEffect, useState } from "react";
2+
import mammoth from "mammoth";
3+
import { DocumentViewerContainerProps } from "typings/DocumentViewerProps";
4+
import { DocRendererElement } from "./documentRenderer";
5+
6+
const DocxViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
7+
const { file } = props;
8+
const [docxHtml, setDocxHtml] = useState<string | null>(null);
9+
10+
const loadContent = useCallback(async (arrayBuffer: any) => {
11+
try {
12+
mammoth
13+
.convertToHtml(
14+
{ arrayBuffer: arrayBuffer },
15+
{
16+
includeDefaultStyleMap: true
17+
}
18+
)
19+
.then(result => {
20+
if (result) {
21+
setDocxHtml(result.value);
22+
}
23+
});
24+
} catch (error) {}
25+
}, []);
26+
27+
useEffect(() => {
28+
const controller = new AbortController();
29+
const { signal } = controller;
30+
if (file.status === "available" && file.value.uri) {
31+
fetch(file.value.uri, { method: "GET", signal })
32+
.then(res => res.arrayBuffer())
33+
.then(response => {
34+
loadContent(response);
35+
});
36+
}
37+
38+
return () => {
39+
controller.abort();
40+
};
41+
}, [file, file?.status, file?.value?.uri]);
42+
43+
return (
44+
<Fragment>
45+
{docxHtml && (
46+
<div
47+
// className={styles['document-container']}
48+
style={{
49+
width: 1 * 100 + "%",
50+
height: "85%",
51+
overflow: "auto"
52+
}}
53+
dangerouslySetInnerHTML={{ __html: docxHtml }}
54+
>
55+
{/* {docHtmlStr} */}
56+
</div>
57+
)}
58+
</Fragment>
59+
);
60+
};
61+
62+
DocxViewer.contentTypes = [
63+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
64+
"application/msword",
65+
"application/vnd.ms-word",
66+
"application/vnd.ms-word.document.macroEnabled.12",
67+
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
68+
"application/vnd.ms-word.template.macroEnabled.12",
69+
"application/vnd.ms-word.document.12",
70+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
71+
"application/octet-stream"
72+
];
73+
74+
export default DocxViewer;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createElement, Fragment, useContext } from "react";
2+
import { DocumentContext } from "store";
3+
import { DocRendererElement } from "./documentRenderer";
4+
5+
const ErrorViewer: DocRendererElement = () => {
6+
const props = useContext(DocumentContext);
7+
console.log("ErrorViewer", props);
8+
return (
9+
<Fragment>
10+
<div className="widget-document-viewer-controls">No document selected</div>
11+
</Fragment>
12+
);
13+
};
14+
15+
ErrorViewer.contentTypes = [];
16+
17+
export default ErrorViewer;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createElement, Fragment, useEffect, useState } from "react";
2+
import { Document, Page, pdfjs } from "react-pdf";
3+
import "react-pdf/dist/Page/AnnotationLayer.css";
4+
import "react-pdf/dist/Page/TextLayer.css";
5+
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
6+
import { DocRendererElement } from "./documentRenderer";
7+
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
8+
const options = {
9+
cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
10+
standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/standard_fonts`
11+
};
12+
13+
const PDFViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
14+
const { file } = props;
15+
const [numberOfPages, setNumberOfPages] = useState<number>(1);
16+
const [currentPage, setCurrentPage] = useState<number>(1);
17+
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
18+
19+
if (!file.value?.uri) {
20+
return <div>No document selected</div>;
21+
}
22+
23+
useEffect(() => {
24+
if (file.status === "available" && file.value.uri) {
25+
setPdfUrl(file.value.uri);
26+
}
27+
}, [file, file.status, file.value.uri]);
28+
29+
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
30+
setNumberOfPages(numPages);
31+
}
32+
33+
return (
34+
<Fragment>
35+
<div className="widget-document-viewer-controls">
36+
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage <= 1}>
37+
Previous
38+
</button>
39+
<span>
40+
Page {currentPage} of {numberOfPages}
41+
</span>
42+
<button onClick={() => setCurrentPage(prev => Math.min(prev + 1, numberOfPages))}>Next</button>
43+
</div>
44+
<div className="widget-document-viewer-content">
45+
{pdfUrl && (
46+
<Document file={pdfUrl} options={options} onLoadSuccess={onDocumentLoadSuccess}>
47+
<Page pageNumber={currentPage} />
48+
</Document>
49+
)}
50+
</div>
51+
</Fragment>
52+
);
53+
};
54+
55+
PDFViewer.contentTypes = ["application/pdf", "application/x-pdf", "application/acrobat", "text/pdf"];
56+
57+
export default PDFViewer;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { FC } from "react";
2+
import { DocumentViewerContainerProps } from "typings/DocumentViewerProps";
3+
4+
export interface DocRendererElement extends FC<DocumentViewerContainerProps> {
5+
contentTypes: string[];
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Docxviewer from "./Docxviewer";
2+
import PDFViewer from "./PDFViewer";
3+
4+
export const DocumentRenderers = [Docxviewer, PDFViewer];

packages/pluggableWidgets/document-viewer-web/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
},
3636
"dependencies": {
3737
"classnames": "^2.3.2",
38+
"mammoth": "^1.9.0",
39+
"pdfjs-dist": "^5.0.375",
3840
"react-pdf": "^9.2.1"
3941
},
4042
"devDependencies": {
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,14 @@
1-
import { createElement, ReactElement, useState } from "react";
1+
import { createElement, ReactElement } from "react";
2+
import { DocumentContext } from "../store";
23
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
3-
import { Document, Page, pdfjs } from "react-pdf";
4-
import "react-pdf/dist/Page/AnnotationLayer.css";
5-
import "react-pdf/dist/Page/TextLayer.css";
6-
7-
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
8-
const options = {
9-
cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
10-
standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/standard_fonts`
11-
};
4+
import { useRendererSelector } from "../utils/useRendererSelector";
125

136
export default function DocumentViewer(props: DocumentViewerContainerProps): ReactElement {
14-
const [numberOfPages, setNumberOfPages] = useState<number>(1);
15-
const [currentPage, setCurrentPage] = useState<number>(1);
16-
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
17-
18-
// Load PDF when URL changes
19-
if (props.file.value?.uri && !pdfUrl) {
20-
setPdfUrl(props.file.value.uri);
21-
}
22-
23-
if (!props.file.value?.uri) {
24-
return <div>No document selected</div>;
25-
}
26-
27-
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
28-
setNumberOfPages(numPages);
29-
}
7+
const { CurrentRenderer } = useRendererSelector(props);
308

319
return (
32-
<div className="widget-document-viewer">
33-
<div className="widget-document-viewer-controls">
34-
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage <= 1}>
35-
Previous
36-
</button>
37-
<span>
38-
Page {currentPage} of {numberOfPages}
39-
</span>
40-
<button onClick={() => setCurrentPage(prev => Math.min(prev + 1, numberOfPages))}>Next</button>
41-
</div>
42-
<div className="widget-document-viewer-content">
43-
<Document file={pdfUrl} options={options} onLoadSuccess={onDocumentLoadSuccess}>
44-
<Page pageNumber={currentPage} />
45-
</Document>
46-
</div>
47-
</div>
10+
<DocumentContext.Provider value={props}>
11+
<div className="widget-document-viewer">{CurrentRenderer && <CurrentRenderer {...props} />}</div>
12+
</DocumentContext.Provider>
4813
);
4914
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createContext } from "react";
2+
import { DocumentViewerContainerProps } from "typings/DocumentViewerProps";
3+
4+
const DocumentContext = createContext<DocumentViewerContainerProps>({} as DocumentViewerContainerProps);
5+
6+
export { DocumentContext };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useEffect, useState } from "react";
2+
3+
import { DocumentRenderers } from "../components";
4+
import { DocRendererElement } from "../components/documentRenderer";
5+
import ErrorViewer from "../components/ErrorViewer";
6+
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
7+
8+
interface DocumentRenderer {
9+
CurrentRenderer: DocRendererElement;
10+
}
11+
12+
export function useRendererSelector(props: DocumentViewerContainerProps): DocumentRenderer {
13+
const { file } = props;
14+
const [component, setComponent] = useState<DocRendererElement>(() => ErrorViewer);
15+
useEffect(() => {
16+
const controller = new AbortController();
17+
const { signal } = controller;
18+
if (file.status === "available" && file.value.uri) {
19+
fetch(file.value.uri, { method: "HEAD", signal }).then(response => {
20+
const contentTypeRaw = response.headers.get("content-type");
21+
const contentTypes = contentTypeRaw?.split(";") || [];
22+
const contentType = contentTypes.length ? contentTypes[0] : undefined;
23+
24+
if (contentType) {
25+
const selectedRenderer: DocRendererElement[] = [];
26+
DocumentRenderers.forEach(renderer => {
27+
if (renderer.contentTypes.includes(contentType)) {
28+
selectedRenderer.push(renderer);
29+
}
30+
});
31+
if (selectedRenderer.length > 0) {
32+
setComponent(() => selectedRenderer[0]);
33+
}
34+
}
35+
});
36+
}
37+
38+
return () => {
39+
controller.abort();
40+
};
41+
}, [file, file?.status, file?.value?.uri]);
42+
return { CurrentRenderer: component };
43+
}

0 commit comments

Comments
 (0)