Skip to content

Commit 9984f81

Browse files
committed
feat(doc-viewer): add dimension, handle error, add xlsx
1 parent 5a3e08b commit 9984f81

20 files changed

+294
-101
lines changed

packages/pluggableWidgets/document-viewer-web/components/BaseViewer.tsx

+45-17
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,77 @@
1-
import React, { createElement, Fragment, PropsWithChildren, ReactElement } from "react";
1+
import React, { createElement, Fragment, PropsWithChildren, ReactElement, useCallback } from "react";
22
import { useZoomScale } from "../utils/useZoomScale";
3-
import { constructWrapperStyle, DimensionContainerProps } from "../utils/dimension";
3+
import { DocumentViewerContainerProps } from "typings/DocumentViewerProps";
4+
import { downloadFile } from "../utils/helpers";
45

5-
interface BaseViewerProps extends PropsWithChildren, DimensionContainerProps {
6+
type FileFormat = {
7+
status: "available";
8+
value: {
9+
uri: string;
10+
name: string;
11+
};
12+
};
13+
14+
interface BaseControlViewerProps extends PropsWithChildren {
15+
file: DocumentViewerContainerProps["file"] | FileFormat;
16+
CustomControl?: React.ReactNode;
17+
}
18+
19+
interface BaseViewerProps extends PropsWithChildren {
620
fileName: string;
721
CustomControl?: React.ReactNode;
822
}
923

10-
const BaseControlViewer = (props: BaseViewerProps): ReactElement => {
24+
const BaseViewer = (props: BaseViewerProps): ReactElement => {
1125
const { fileName, CustomControl, children } = props;
12-
const wrapperStyle = constructWrapperStyle(props);
1326
return (
1427
<Fragment>
1528
<div className="widget-document-viewer-controls">
16-
<div className="widget-document-viewer-controls-left">{fileName}</div>
29+
<div className="widget-document-viewer-controls-left">
30+
<div className="document-title">{fileName}</div>
31+
</div>
1732
<div className="widget-document-viewer-controls-icons">{CustomControl}</div>
1833
</div>
19-
<div className="widget-document-viewer-content" style={wrapperStyle}>
20-
{children}
21-
</div>
34+
<div className="widget-document-viewer-content">{children}</div>
2235
</Fragment>
2336
);
2437
};
2538

26-
const BaseViewer = (props: BaseViewerProps): ReactElement => {
27-
const { CustomControl, children } = props;
28-
const { zoomLevel, zoomIn, zoomOut } = useZoomScale();
39+
const BaseControlViewer = (props: BaseControlViewerProps): ReactElement => {
40+
const { CustomControl, children, file } = props;
41+
const { zoomLevel, zoomIn, zoomOut, reset } = useZoomScale();
42+
const onDownloadClick = useCallback(() => {
43+
downloadFile(file.value?.uri);
44+
}, [file]);
45+
2946
return (
30-
<BaseControlViewer
31-
{...props}
47+
<BaseViewer
48+
fileName={file.value?.name || ""}
3249
CustomControl={
3350
<Fragment>
3451
{CustomControl}
52+
<button
53+
onClick={onDownloadClick}
54+
className="icons icon-Download btn btn-icon-only"
55+
aria-label={"download"}
56+
></button>
3557
<div className="widget-document-viewer-zoom">
3658
<button
3759
onClick={zoomOut}
3860
disabled={zoomLevel <= 0.3}
3961
className="icons icon-ZoomOut btn btn-icon-only"
40-
aria-label={"Go to previous page"}
62+
aria-label={"Zoom out"}
4163
></button>
4264
<button
4365
onClick={zoomIn}
4466
disabled={zoomLevel >= 10}
4567
className="icons icon-ZoomIn btn btn-icon-only"
46-
aria-label={"Go to previous page"}
68+
aria-label={"Zoom in"}
69+
></button>
70+
<button
71+
onClick={reset}
72+
disabled={zoomLevel >= 10}
73+
className="icons icon-FitToWidth btn btn-icon-only"
74+
aria-label={"Fit to width"}
4775
></button>
4876
</div>
4977
</Fragment>
@@ -55,7 +83,7 @@ const BaseViewer = (props: BaseViewerProps): ReactElement => {
5583
>
5684
{children}
5785
</div>
58-
</BaseControlViewer>
86+
</BaseViewer>
5987
);
6088
};
6189

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.widget-document-viewer {
2+
section.docx-viewer-content {
3+
padding: var(--spacing-largest, 48pt) !important;
4+
}
5+
}

packages/pluggableWidgets/document-viewer-web/components/DocxViewer.tsx

+41-28
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,43 @@
1-
import mammoth from "mammoth";
2-
import { createElement, useCallback, useEffect, useState } from "react";
3-
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
1+
import { parseAsync, renderDocument, WordDocument, Options } from "docx-preview";
2+
import { createElement, useCallback, useEffect, useRef } from "react";
43
import { BaseControlViewer } from "./BaseViewer";
5-
import { DocRendererElement } from "./documentRenderer";
4+
import { DocRendererElement, DocumentRendererProps, DocumentStatus } from "./documentRenderer";
5+
import "./DocxViewer.scss";
66

7-
const DocxViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
8-
const { file } = props;
9-
const [docxHtml, setDocxHtml] = useState<string | null>(null);
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: any) => {
20-
if (result) {
21-
setDocxHtml(result.value);
22-
}
23-
});
24-
} catch (error) {
25-
setDocxHtml(`<div>Error loading file : ${error}</div>`);
26-
}
27-
}, []);
7+
const DOC_CONFIG: Partial<Options> = {
8+
className: "docx-viewer-content",
9+
ignoreWidth: true,
10+
ignoreLastRenderedPageBreak: false,
11+
inWrapper: false
12+
};
13+
14+
const DocxViewer: DocRendererElement = (props: DocumentRendererProps) => {
15+
const { file, setDocumentStatus } = props;
16+
const localRef = useRef<HTMLDivElement>(null);
17+
const loadContent = useCallback(
18+
async (arrayBuffer: any) => {
19+
try {
20+
parseAsync(arrayBuffer, DOC_CONFIG)
21+
.then((wordDocument: WordDocument) => {
22+
if (localRef.current) {
23+
// create new dummy stylecontainer to be ignored
24+
const styleContainer = document.createElement("div");
25+
renderDocument(wordDocument, localRef.current, styleContainer, DOC_CONFIG).catch(
26+
(_error: any) => {
27+
setDocumentStatus(DocumentStatus.error);
28+
}
29+
);
30+
}
31+
})
32+
.catch((_error: any) => {
33+
setDocumentStatus(DocumentStatus.error);
34+
});
35+
} catch (_error: any) {
36+
setDocumentStatus(DocumentStatus.error);
37+
}
38+
},
39+
[setDocumentStatus]
40+
);
2841

2942
useEffect(() => {
3043
const controller = new AbortController();
@@ -41,11 +54,11 @@ const DocxViewer: DocRendererElement = (props: DocumentViewerContainerProps) =>
4154
return () => {
4255
controller.abort();
4356
};
44-
}, [file, file?.status, file?.value?.uri]);
57+
}, [file, file.status, file.value?.uri, loadContent]);
4558

4659
return (
47-
<BaseControlViewer {...props} fileName={file.value?.name || ""}>
48-
{docxHtml && <div className="docx-viewer-content" dangerouslySetInnerHTML={{ __html: docxHtml }}></div>}
60+
<BaseControlViewer {...props} file={file}>
61+
<div className="docx-viewer-container" ref={localRef}></div>
4962
</BaseControlViewer>
5063
);
5164
};

packages/pluggableWidgets/document-viewer-web/components/ErrorViewer.tsx

+30-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,34 @@
1-
import { createElement } from "react";
2-
import { DocRendererElement } from "./documentRenderer";
1+
import { createElement, useCallback } from "react";
2+
import { DocRendererElement, DocumentRendererProps } from "./documentRenderer";
3+
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
4+
import BaseViewer from "./BaseViewer";
5+
import { downloadFile } from "../utils/helpers";
36

4-
const ErrorViewer: DocRendererElement = () => {
5-
return <div className="widget-document-viewer-content">No document selected</div>;
7+
const ErrorViewer: DocRendererElement = (props: DocumentRendererProps) => {
8+
const { file } = props;
9+
const onDownloadClick = useCallback(() => {
10+
downloadFile(file.value?.uri);
11+
}, [file]);
12+
13+
return (
14+
<BaseViewer
15+
{...props}
16+
fileName={file.value?.name || ""}
17+
CustomControl={
18+
<button
19+
onClick={onDownloadClick}
20+
className="icons icon-Download btn btn-icon-only"
21+
aria-label={"download"}
22+
></button>
23+
}
24+
>
25+
{file.status === "available" ? (
26+
<div>{"Unsupported document type"}</div>
27+
) : (
28+
<div className="widget-document-viewer-loading"></div>
29+
)}
30+
</BaseViewer>
31+
);
632
};
733

834
ErrorViewer.contentTypes = [];

packages/pluggableWidgets/document-viewer-web/components/ExcelViewer.tsx

+17-15
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import { createElement, useCallback, useEffect, useState } from "react";
22
import { read, utils } from "xlsx";
3-
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
43
import { BaseControlViewer } from "./BaseViewer";
5-
import { DocRendererElement } from "./documentRenderer";
4+
import { DocRendererElement, DocumentRendererProps, DocumentStatus } from "./documentRenderer";
65

7-
const ExcelViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
8-
const { file } = props;
6+
const ExcelViewer: DocRendererElement = (props: DocumentRendererProps) => {
7+
const { file, setDocumentStatus } = props;
98
const [xlsxHtml, setXlsxHtml] = useState<string | null>(null);
109

11-
const loadContent = useCallback(async (arrayBuffer: any) => {
12-
try {
13-
const wb = read(arrayBuffer);
14-
const sheet = wb.Sheets[wb.SheetNames[0]];
15-
const html = utils.sheet_to_html(sheet);
16-
setXlsxHtml(html);
17-
} catch (error) {
18-
setXlsxHtml(`<div>Error loading file : ${error}</div>`);
19-
}
20-
}, []);
10+
const loadContent = useCallback(
11+
async (arrayBuffer: any) => {
12+
try {
13+
const wb = read(arrayBuffer);
14+
const sheet = wb.Sheets[wb.SheetNames[0]];
15+
const html = utils.sheet_to_html(sheet);
16+
setXlsxHtml(html);
17+
} catch (_error) {
18+
setDocumentStatus(DocumentStatus.error);
19+
}
20+
},
21+
[setDocumentStatus]
22+
);
2123

2224
useEffect(() => {
2325
const controller = new AbortController();
@@ -36,7 +38,7 @@ const ExcelViewer: DocRendererElement = (props: DocumentViewerContainerProps) =>
3638
}, [file, file.status, file.value?.uri, loadContent]);
3739

3840
return (
39-
<BaseControlViewer {...props} fileName={file.value?.name || ""}>
41+
<BaseControlViewer {...props} file={file}>
4042
{xlsxHtml && <div className="xlsx-viewer-content" dangerouslySetInnerHTML={{ __html: xlsxHtml }}></div>}
4143
</BaseControlViewer>
4244
);

packages/pluggableWidgets/document-viewer-web/components/PDFViewer.tsx

+27-7
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
1-
import { createElement, Fragment, useEffect, useState } from "react";
1+
import { createElement, Fragment, useCallback, useEffect, useState } from "react";
22
import { Document, Page, pdfjs } from "react-pdf";
33
import "react-pdf/dist/Page/AnnotationLayer.css";
44
import "react-pdf/dist/Page/TextLayer.css";
5-
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
6-
import { DocRendererElement } from "./documentRenderer";
5+
import { downloadFile } from "../utils/helpers";
76
import { useZoomScale } from "../utils/useZoomScale";
87
import BaseViewer from "./BaseViewer";
8+
import { DocRendererElement, DocumentRendererProps, DocumentStatus } from "./documentRenderer";
99
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
1010
const options = {
1111
cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
1212
standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/standard_fonts`
1313
};
1414

15-
const PDFViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
16-
const { file } = props;
15+
const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => {
16+
const { file, setDocumentStatus } = props;
1717
const [numberOfPages, setNumberOfPages] = useState<number>(1);
18-
const { zoomLevel, zoomIn, zoomOut } = useZoomScale();
18+
const { zoomLevel, zoomIn, zoomOut, reset } = useZoomScale();
1919
const [currentPage, setCurrentPage] = useState<number>(1);
2020
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
2121

22+
const onDownloadClick = useCallback(() => {
23+
downloadFile(file.value?.uri);
24+
}, [file]);
25+
2226
useEffect(() => {
2327
if (file.status === "available" && file.value.uri) {
2428
setPdfUrl(file.value.uri);
@@ -55,6 +59,11 @@ const PDFViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
5559
aria-label={"Go to next page"}
5660
></button>
5761
</div>
62+
<button
63+
onClick={onDownloadClick}
64+
className="icons icon-Download btn btn-icon-only"
65+
aria-label={"download"}
66+
></button>
5867
<div className="widget-document-viewer-zoom">
5968
<button
6069
onClick={zoomOut}
@@ -68,11 +77,22 @@ const PDFViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
6877
className="icons icon-ZoomIn btn btn-icon-only"
6978
aria-label={"Go to previous page"}
7079
></button>
80+
<button
81+
onClick={reset}
82+
disabled={zoomLevel >= 10}
83+
className="icons icon-FitToWidth btn btn-icon-only"
84+
aria-label={"Fit to width"}
85+
></button>
7186
</div>
7287
</Fragment>
7388
}
7489
>
75-
<Document file={pdfUrl} options={options} onLoadSuccess={onDocumentLoadSuccess}>
90+
<Document
91+
file={pdfUrl}
92+
options={options}
93+
onLoadSuccess={onDocumentLoadSuccess}
94+
onLoadError={() => setDocumentStatus(DocumentStatus.error)}
95+
>
7696
<Page pageNumber={currentPage} scale={zoomLevel} />
7797
</Document>
7898
</BaseViewer>
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import { FC } from "react";
22
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
33

4-
export interface DocRendererElement extends FC<DocumentViewerContainerProps> {
4+
export declare const enum DocumentStatus {
5+
available = "available",
6+
error = "error",
7+
loading = "loading"
8+
}
9+
10+
export interface DocumentRendererProps extends DocumentViewerContainerProps {
11+
setDocumentStatus: (status: DocumentStatus) => void;
12+
}
13+
14+
export interface DocRendererElement extends FC<DocumentRendererProps> {
515
contentTypes: string[];
616
fileTypes: string[];
717
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
},
3636
"dependencies": {
3737
"classnames": "^2.3.2",
38+
"docx-preview": "^0.3.5",
3839
"mammoth": "github:uicontent/mammoth",
3940
"pdfjs-dist": "^5.0.375",
4041
"react-pdf": "^9.2.1",

packages/pluggableWidgets/document-viewer-web/src/DocumentViewer.editorConfig.ts

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {
66
import { DocumentViewerPreviewProps } from "typings/DocumentViewerProps";
77

88
export function getProperties(values: DocumentViewerPreviewProps, defaultProperties: Properties): Properties {
9+
if (values.widthUnit === "contentFit") {
10+
hidePropertyIn(defaultProperties, values, "width");
11+
}
912
if (values.heightUnit === "percentageOfWidth") {
1013
hidePropertyIn(defaultProperties, values, "height");
1114
} else {

0 commit comments

Comments
 (0)