Skip to content

[WC-2890] doc viewer: add docx support #1519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 10, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createElement, Fragment, useCallback, useEffect, useState } from "react";
import mammoth from "mammoth";
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
import { DocRendererElement } from "./documentRenderer";

const DocxViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
const { file } = props;
const [docxHtml, setDocxHtml] = useState<string | null>(null);

const loadContent = useCallback(async (arrayBuffer: any) => {
try {
mammoth
.convertToHtml(
{ arrayBuffer: arrayBuffer },
{
includeDefaultStyleMap: true
}
)
.then((result: any) => {
if (result) {
setDocxHtml(result.value);
}
});
} catch (error) {}
}, []);

useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
if (file.status === "available" && file.value.uri) {
fetch(file.value.uri, { method: "GET", signal })
.then(res => res.arrayBuffer())
.then(response => {
loadContent(response);
});
}

return () => {
controller.abort();
};
}, [file, file?.status, file?.value?.uri]);

return (
<Fragment>
<div className="widget-document-viewer-controls">
<div className="widget-document-viewer-controls-left">{file.value?.name}</div>
</div>
{docxHtml && (
<div className="widget-document-viewer-content" dangerouslySetInnerHTML={{ __html: docxHtml }}>
{/* {docHtmlStr} */}
</div>
)}
</Fragment>
);
};

DocxViewer.contentTypes = [
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"application/vnd.ms-word",
"application/vnd.ms-word.document.macroEnabled.12",
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
"application/vnd.ms-word.template.macroEnabled.12",
"application/vnd.ms-word.document.12",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/octet-stream"
];

export default DocxViewer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createElement, Fragment } from "react";
import { DocRendererElement } from "./documentRenderer";

const ErrorViewer: DocRendererElement = () => {
return (
<Fragment>
<div className="widget-document-viewer-content">No document selected</div>
</Fragment>
);
};

ErrorViewer.contentTypes = [];

export default ErrorViewer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createElement, Fragment, useEffect, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
import "react-pdf/dist/Page/AnnotationLayer.css";
import "react-pdf/dist/Page/TextLayer.css";
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
import { DocRendererElement } from "./documentRenderer";
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
const options = {
cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/standard_fonts`
};

const PDFViewer: DocRendererElement = (props: DocumentViewerContainerProps) => {
const { file } = props;
const [numberOfPages, setNumberOfPages] = useState<number>(1);
const [currentPage, setCurrentPage] = useState<number>(1);
const [pdfUrl, setPdfUrl] = useState<string | null>(null);

if (!file.value?.uri) {
return <div>No document selected</div>;
}

useEffect(() => {
if (file.status === "available" && file.value.uri) {
setPdfUrl(file.value.uri);
}
}, [file, file.status, file.value.uri]);

function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
setNumberOfPages(numPages);
}

return (
<Fragment>
<div className="widget-document-viewer-controls">
<div className="widget-document-viewer-controls-left">{file.value?.name}</div>
<div className="widget-document-viewer-controls-icons">
<div className="widget-document-viewer-pagination">
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage <= 1}
className="icons icon-Left btn btn-icon-only"
aria-label={"Go to previous page"}
></button>
<span>
{currentPage} / {numberOfPages}
</span>
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, numberOfPages))}
className="icons icon-Right btn btn-icon-only"
aria-label={"Go to next page"}
></button>
</div>
</div>
</div>
<div className="widget-document-viewer-content">
{pdfUrl && (
<Document file={pdfUrl} options={options} onLoadSuccess={onDocumentLoadSuccess}>
<Page pageNumber={currentPage} />
</Document>
)}
</div>
</Fragment>
);
};

PDFViewer.contentTypes = ["application/pdf", "application/x-pdf", "application/acrobat", "text/pdf"];

export default PDFViewer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { FC } from "react";
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";

export interface DocRendererElement extends FC<DocumentViewerContainerProps> {
contentTypes: string[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import DocxViewer from "./DocxViewer";
import PDFViewer from "./PDFViewer";

export const DocumentRenderers = [DocxViewer, PDFViewer];
Original file line number Diff line number Diff line change
@@ -35,12 +35,15 @@
},
"dependencies": {
"classnames": "^2.3.2",
"mammoth": "github:uicontent/mammoth",
"pdfjs-dist": "^5.0.375",
"react-pdf": "^9.2.1"
},
"devDependencies": {
"@babel/plugin-transform-class-properties": "^7.23.3",
"@babel/plugin-transform-private-methods": "^7.23.3",
"@babel/plugin-transform-private-property-in-object": "^7.23.4",
"@mendix/pluggable-widgets-tools": "^10.0.0"
"@mendix/pluggable-widgets-tools": "^10.0.0",
"@rollup/plugin-replace": "^6.0.2"
}
}

This file was deleted.

48 changes: 48 additions & 0 deletions packages/pluggableWidgets/document-viewer-web/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import commonjs from "@rollup/plugin-commonjs";
import replace from "@rollup/plugin-replace";

export default args => {
const result = args.configDefaultConfig;
return result.map((config, _index) => {
config.output.inlineDynamicImports = true;
if (config.output.format !== "es") {
return config;
}
return {
...config,
plugins: [
...config.plugins.map(plugin => {
if (plugin && plugin.name === "commonjs") {
// replace common js plugin that transforms
// external requires to imports
// this is needed in order to work with modern client
return commonjs({
extensions: [".js", ".jsx", ".tsx", ".ts"],
transformMixedEsModules: true,
requireReturnsDefault: "auto",
esmExternals: true
});
}

return plugin;
}),
// rollup config for pdfjs-dist copying from https://github.com/unjs/unpdf/blob/main/pdfjs.rollup.config.ts
replace({
delimiters: ["", ""],
preventAssignment: true,
values: {
// Disable the `window` check (for requestAnimationFrame).
"typeof window": '"undefined"',
// Imitate the Node.js environment for all serverless environments, unenv will
// take care of the remaining Node.js polyfills. Keep support for browsers.
"const isNodeJS = typeof": 'const isNodeJS = typeof document === "undefined" // typeof',
// Force inlining the PDF.js worker.
"await import(/*webpackIgnore: true*/this.workerSrc)": "__pdfjsWorker__",
// Tree-shake client worker initialization logic.
"!PDFWorkerUtil.isWorkerDisabled && !PDFWorker.#mainThreadWorkerMessageHandler": "false"
}
})
]
};
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Properties } from "@mendix/pluggable-widgets-tools";
import {
StructurePreviewProps,
structurePreviewPalette
} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
import { DocumentViewerPreviewProps } from "typings/DocumentViewerProps";

export function getProperties(_values: DocumentViewerPreviewProps, defaultProperties: Properties): Properties {
return defaultProperties;
}

export function getPreview(_values: DocumentViewerPreviewProps, isDarkMode: boolean): StructurePreviewProps {
const palette = structurePreviewPalette[isDarkMode ? "dark" : "light"];
return {
type: "Container",
children: [
{
type: "RowLayout",
grow: 2,
columnSize: "grow",
borders: true,
borderWidth: 1,
borderRadius: 2,
backgroundColor: _values.readOnly ? palette.background.containerDisabled : palette.background.container,
children: [
{
type: "Container",
grow: 1,
padding: 4,
children: [
{
type: "Text",
content: getCustomCaption(_values),
fontColor: palette.text.data
}
]
}
],
padding: 8
}
],
backgroundColor: palette.background.container,
borderRadius: 8
};
}

export function getCustomCaption(_values: DocumentViewerPreviewProps): string {
return `[${_values.file ?? "No attribute selected"}]`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createElement, ReactElement } from "react";
import { DocumentViewerPreviewProps } from "typings/DocumentViewerProps";
import "../ui/documentViewer.scss";

export const preview = (props: DocumentViewerPreviewProps): ReactElement => {
const { file } = props;
return (
<div className="widget-document-viewer">
<div className="widget-document-viewer-content">{file ? `[${file}]` : "[No attribute selected]"}</div>
</div>
);
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,49 +1,19 @@
import { createElement, ReactElement, useState } from "react";
import { createElement, ReactElement } from "react";
import { DocumentContext } from "../store";
import { DocumentViewerContainerProps } from "../typings/DocumentViewerProps";
import { Document, Page, pdfjs } from "react-pdf";
import "react-pdf/dist/Page/AnnotationLayer.css";
import "react-pdf/dist/Page/TextLayer.css";

pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
const options = {
cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/standard_fonts`
};
import { useRendererSelector } from "../utils/useRendererSelector";
import "../ui/documentViewer.scss";
import "../ui/documentViewerIcons.scss";
import classNames from "classnames";

export default function DocumentViewer(props: DocumentViewerContainerProps): ReactElement {
const [numberOfPages, setNumberOfPages] = useState<number>(1);
const [currentPage, setCurrentPage] = useState<number>(1);
const [pdfUrl, setPdfUrl] = useState<string | null>(null);

// Load PDF when URL changes
if (props.file.value?.uri && !pdfUrl) {
setPdfUrl(props.file.value.uri);
}

if (!props.file.value?.uri) {
return <div>No document selected</div>;
}

function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
setNumberOfPages(numPages);
}
const { CurrentRenderer } = useRendererSelector(props);

return (
<div className="widget-document-viewer">
<div className="widget-document-viewer-controls">
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage <= 1}>
Previous
</button>
<span>
Page {currentPage} of {numberOfPages}
</span>
<button onClick={() => setCurrentPage(prev => Math.min(prev + 1, numberOfPages))}>Next</button>
</div>
<div className="widget-document-viewer-content">
<Document file={pdfUrl} options={options} onLoadSuccess={onDocumentLoadSuccess}>
<Page pageNumber={currentPage} />
</Document>
<DocumentContext.Provider value={props}>
<div className={classNames(props.class, "widget-document-viewer", "form-control")}>
{CurrentRenderer && <CurrentRenderer {...props} />}
</div>
</div>
</DocumentContext.Provider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createContext } from "react";
import { DocumentViewerContainerProps } from "typings/DocumentViewerProps";

const DocumentContext = createContext<DocumentViewerContainerProps>({} as DocumentViewerContainerProps);

export { DocumentContext };
Loading
Loading