Skip to content

Commit 5a3e08b

Browse files
committed
chore(doc-viewer): add zoom controls
1 parent 16fdb9d commit 5a3e08b

17 files changed

+391
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { createElement, Fragment, PropsWithChildren, ReactElement } from "react";
2+
import { useZoomScale } from "../utils/useZoomScale";
3+
import { constructWrapperStyle, DimensionContainerProps } from "../utils/dimension";
4+
5+
interface BaseViewerProps extends PropsWithChildren, DimensionContainerProps {
6+
fileName: string;
7+
CustomControl?: React.ReactNode;
8+
}
9+
10+
const BaseControlViewer = (props: BaseViewerProps): ReactElement => {
11+
const { fileName, CustomControl, children } = props;
12+
const wrapperStyle = constructWrapperStyle(props);
13+
return (
14+
<Fragment>
15+
<div className="widget-document-viewer-controls">
16+
<div className="widget-document-viewer-controls-left">{fileName}</div>
17+
<div className="widget-document-viewer-controls-icons">{CustomControl}</div>
18+
</div>
19+
<div className="widget-document-viewer-content" style={wrapperStyle}>
20+
{children}
21+
</div>
22+
</Fragment>
23+
);
24+
};
25+
26+
const BaseViewer = (props: BaseViewerProps): ReactElement => {
27+
const { CustomControl, children } = props;
28+
const { zoomLevel, zoomIn, zoomOut } = useZoomScale();
29+
return (
30+
<BaseControlViewer
31+
{...props}
32+
CustomControl={
33+
<Fragment>
34+
{CustomControl}
35+
<div className="widget-document-viewer-zoom">
36+
<button
37+
onClick={zoomOut}
38+
disabled={zoomLevel <= 0.3}
39+
className="icons icon-ZoomOut btn btn-icon-only"
40+
aria-label={"Go to previous page"}
41+
></button>
42+
<button
43+
onClick={zoomIn}
44+
disabled={zoomLevel >= 10}
45+
className="icons icon-ZoomIn btn btn-icon-only"
46+
aria-label={"Go to previous page"}
47+
></button>
48+
</div>
49+
</Fragment>
50+
}
51+
>
52+
<div
53+
className="widget-document-viewer-zoom-container"
54+
style={{ "--default-zoom-scale": zoomLevel } as React.CSSProperties}
55+
>
56+
{children}
57+
</div>
58+
</BaseControlViewer>
59+
);
60+
};
61+
62+
export default BaseViewer;
63+
export { BaseControlViewer };

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

+11-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { createElement, Fragment, useCallback, useEffect, useState } from "react";
21
import mammoth from "mammoth";
2+
import { createElement, useCallback, useEffect, useState } from "react";
33
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
4+
import { BaseControlViewer } from "./BaseViewer";
45
import { DocRendererElement } from "./documentRenderer";
56

67
const DocxViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
78
const { file } = props;
89
const [docxHtml, setDocxHtml] = useState<string | null>(null);
9-
1010
const loadContent = useCallback(async (arrayBuffer: any) => {
1111
try {
1212
mammoth
@@ -21,13 +21,16 @@ const DocxViewer: DocRendererElement = (props: DocumentViewerContainerProps) =>
2121
setDocxHtml(result.value);
2222
}
2323
});
24-
} catch (error) {}
24+
} catch (error) {
25+
setDocxHtml(`<div>Error loading file : ${error}</div>`);
26+
}
2527
}, []);
2628

2729
useEffect(() => {
2830
const controller = new AbortController();
2931
const { signal } = controller;
3032
if (file.status === "available" && file.value.uri) {
33+
console.log("fetch file", file.value.uri);
3134
fetch(file.value.uri, { method: "GET", signal })
3235
.then(res => res.arrayBuffer())
3336
.then(response => {
@@ -41,16 +44,9 @@ const DocxViewer: DocRendererElement = (props: DocumentViewerContainerProps) =>
4144
}, [file, file?.status, file?.value?.uri]);
4245

4346
return (
44-
<Fragment>
45-
<div className="widget-document-viewer-controls">
46-
<div className="widget-document-viewer-controls-left">{file.value?.name}</div>
47-
</div>
48-
{docxHtml && (
49-
<div className="widget-document-viewer-content" dangerouslySetInnerHTML={{ __html: docxHtml }}>
50-
{/* {docHtmlStr} */}
51-
</div>
52-
)}
53-
</Fragment>
47+
<BaseControlViewer {...props} fileName={file.value?.name || ""}>
48+
{docxHtml && <div className="docx-viewer-content" dangerouslySetInnerHTML={{ __html: docxHtml }}></div>}
49+
</BaseControlViewer>
5450
);
5551
};
5652

@@ -66,4 +62,6 @@ DocxViewer.contentTypes = [
6662
"application/octet-stream"
6763
];
6864

65+
DocxViewer.fileTypes = ["docx"];
66+
6967
export default DocxViewer;
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import { createElement, Fragment } from "react";
1+
import { createElement } from "react";
22
import { DocRendererElement } from "./documentRenderer";
33

44
const ErrorViewer: DocRendererElement = () => {
5-
return (
6-
<Fragment>
7-
<div className="widget-document-viewer-content">No document selected</div>
8-
</Fragment>
9-
);
5+
return <div className="widget-document-viewer-content">No document selected</div>;
106
};
117

128
ErrorViewer.contentTypes = [];
9+
ErrorViewer.fileTypes = [];
1310

1411
export default ErrorViewer;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createElement, useCallback, useEffect, useState } from "react";
2+
import { read, utils } from "xlsx";
3+
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
4+
import { BaseControlViewer } from "./BaseViewer";
5+
import { DocRendererElement } from "./documentRenderer";
6+
7+
const ExcelViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
8+
const { file } = props;
9+
const [xlsxHtml, setXlsxHtml] = useState<string | null>(null);
10+
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+
}, []);
21+
22+
useEffect(() => {
23+
const controller = new AbortController();
24+
const { signal } = controller;
25+
if (file.status === "available" && file.value.uri) {
26+
fetch(file.value.uri, { method: "GET", signal })
27+
.then(res => res.arrayBuffer())
28+
.then(response => {
29+
loadContent(response);
30+
});
31+
}
32+
33+
return () => {
34+
controller.abort();
35+
};
36+
}, [file, file.status, file.value?.uri, loadContent]);
37+
38+
return (
39+
<BaseControlViewer {...props} fileName={file.value?.name || ""}>
40+
{xlsxHtml && <div className="xlsx-viewer-content" dangerouslySetInnerHTML={{ __html: xlsxHtml }}></div>}
41+
</BaseControlViewer>
42+
);
43+
};
44+
45+
ExcelViewer.contentTypes = [
46+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
47+
"application/vnd.ms-excel",
48+
"application/vnd.ms-excel.sheet.macroEnabled.12",
49+
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
50+
"application/vnd.ms-excel.template.macroEnabled.12",
51+
"application/vnd.ms-excel.addin.macroEnabled.12",
52+
"application/octet-stream"
53+
];
54+
55+
ExcelViewer.fileTypes = ["xlsx"];
56+
57+
export default ExcelViewer;

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

+37-20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import "react-pdf/dist/Page/AnnotationLayer.css";
44
import "react-pdf/dist/Page/TextLayer.css";
55
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
66
import { DocRendererElement } from "./documentRenderer";
7+
import { useZoomScale } from "../utils/useZoomScale";
8+
import BaseViewer from "./BaseViewer";
79
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
810
const options = {
911
cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
@@ -13,28 +15,30 @@ const options = {
1315
const PDFViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
1416
const { file } = props;
1517
const [numberOfPages, setNumberOfPages] = useState<number>(1);
18+
const { zoomLevel, zoomIn, zoomOut } = useZoomScale();
1619
const [currentPage, setCurrentPage] = useState<number>(1);
1720
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
1821

19-
if (!file.value?.uri) {
20-
return <div>No document selected</div>;
21-
}
22-
2322
useEffect(() => {
2423
if (file.status === "available" && file.value.uri) {
2524
setPdfUrl(file.value.uri);
2625
}
27-
}, [file, file.status, file.value.uri]);
26+
}, [file, file.status, file.value?.uri]);
2827

2928
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
3029
setNumberOfPages(numPages);
3130
}
3231

32+
if (!file.value?.uri) {
33+
return <div>No document selected</div>;
34+
}
35+
3336
return (
34-
<Fragment>
35-
<div className="widget-document-viewer-controls">
36-
<div className="widget-document-viewer-controls-left">{file.value?.name}</div>
37-
<div className="widget-document-viewer-controls-icons">
37+
<BaseViewer
38+
{...props}
39+
fileName={file.value?.name || ""}
40+
CustomControl={
41+
<Fragment>
3842
<div className="widget-document-viewer-pagination">
3943
<button
4044
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
@@ -51,19 +55,32 @@ const PDFViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
5155
aria-label={"Go to next page"}
5256
></button>
5357
</div>
54-
</div>
55-
</div>
56-
<div className="widget-document-viewer-content">
57-
{pdfUrl && (
58-
<Document file={pdfUrl} options={options} onLoadSuccess={onDocumentLoadSuccess}>
59-
<Page pageNumber={currentPage} />
60-
</Document>
61-
)}
62-
</div>
63-
</Fragment>
58+
<div className="widget-document-viewer-zoom">
59+
<button
60+
onClick={zoomOut}
61+
disabled={zoomLevel <= 0.3}
62+
className="icons icon-ZoomOut btn btn-icon-only"
63+
aria-label={"Go to previous page"}
64+
></button>
65+
<button
66+
onClick={zoomIn}
67+
disabled={zoomLevel >= 10}
68+
className="icons icon-ZoomIn btn btn-icon-only"
69+
aria-label={"Go to previous page"}
70+
></button>
71+
</div>
72+
</Fragment>
73+
}
74+
>
75+
<Document file={pdfUrl} options={options} onLoadSuccess={onDocumentLoadSuccess}>
76+
<Page pageNumber={currentPage} scale={zoomLevel} />
77+
</Document>
78+
</BaseViewer>
6479
);
6580
};
6681

67-
PDFViewer.contentTypes = ["application/pdf", "application/x-pdf", "application/acrobat", "text/pdf"];
82+
PDFViewer.contentTypes = ["application/pdf", "application/x-pdf", "application/acrobat", "text/pdf", "text/html"];
83+
84+
PDFViewer.fileTypes = ["pdf", "pdfx"];
6885

6986
export default PDFViewer;

packages/pluggableWidgets/document-viewer-web/components/documentRenderer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
33

44
export interface DocRendererElement extends FC<DocumentViewerContainerProps> {
55
contentTypes: string[];
6+
fileTypes: string[];
67
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import DocxViewer from "./DocxViewer";
22
import PDFViewer from "./PDFViewer";
3+
import ExcelViewer from "./ExcelViewer";
34

4-
export const DocumentRenderers = [DocxViewer, PDFViewer];
5+
export const DocumentRenderers = [DocxViewer, ExcelViewer, PDFViewer];

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"classnames": "^2.3.2",
3838
"mammoth": "github:uicontent/mammoth",
3939
"pdfjs-dist": "^5.0.375",
40-
"react-pdf": "^9.2.1"
40+
"react-pdf": "^9.2.1",
41+
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
4142
},
4243
"devDependencies": {
4344
"@babel/plugin-transform-class-properties": "^7.23.3",

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

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
1-
import { Properties } from "@mendix/pluggable-widgets-tools";
1+
import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools";
22
import {
33
StructurePreviewProps,
44
structurePreviewPalette
55
} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
66
import { DocumentViewerPreviewProps } from "typings/DocumentViewerProps";
77

8-
export function getProperties(_values: DocumentViewerPreviewProps, defaultProperties: Properties): Properties {
8+
export function getProperties(values: DocumentViewerPreviewProps, defaultProperties: Properties): Properties {
9+
if (values.heightUnit === "percentageOfWidth") {
10+
hidePropertyIn(defaultProperties, values, "height");
11+
} else {
12+
hidePropertiesIn(defaultProperties, values, [
13+
"minHeight",
14+
"minHeightUnit",
15+
"maxHeight",
16+
"maxHeightUnit",
17+
"OverflowY"
18+
]);
19+
}
20+
21+
if (values.minHeightUnit === "none") {
22+
hidePropertyIn(defaultProperties, values, "minHeight");
23+
}
24+
25+
if (values.maxHeightUnit === "none") {
26+
hidePropertiesIn(defaultProperties, values, ["maxHeight", "OverflowY"]);
27+
}
28+
929
return defaultProperties;
1030
}
1131

0 commit comments

Comments
 (0)