UNPKG

react-docx

Version:

React Reconciler for DOCX - build Docx with JSX

232 lines (201 loc) 6.43 kB
import ReactReconciler from "react-reconciler"; import * as Docx from "docx"; import { is } from "../utils/is.js"; import { DocxTypes } from "./elements.js"; const hostConfig = { // _props contain raw react children too createInstance( type, _props, rootContainerInstance, hostContext // internalInstanceHandle - fiber root, we don't need any react magic for now ) { //locate instance constructor in DocxTypes const classConstructor = DocxTypes[type]; // sanitise children away from props // eslint-disable-next-line no-unused-vars const { children = [], ...props } = _props; if (classConstructor) { const params = { text: classConstructor.name === "TextRun" && (is.str(children) || is.num(children)) ? children : undefined, /// for textrun element with singular text child we pass text arg // all custom params above will be overriden by user props, but below ones won't ...props, rows: [], children: [], // some docx elements require children param __document: hostContext.document, // pass document reference for fictive elements }; let docxInstance; // either call class constructor or just a function try { docxInstance = classConstructor.name ? new classConstructor(params) : classConstructor(params); } catch (error) { console.error(error); } Object.keys(props) .filter((p) => is.fun(docxInstance[p])) .forEach((prop) => { const propVal = props[prop]; const propFun = docxInstance[prop]; docxInstance = is.arr(propVal) ? propFun.apply(docxInstance, propVal) // array was passed as prop val so we treat as arguments : propFun.call(docxInstance, propVal); // whaterver else was passed is single argument }); return docxInstance; } throw new Error(`${type} is not Docx Element`); }, // both parentInstance and child are types of host elements (DOCX objects) appendInitialChild(parentInstance, child) { if ("addChildElement" in parentInstance) { parentInstance.addChildElement(child); } else { throw new Error( `${ parentInstance?.type ?? parentInstance?.prototype?.constructor.name ?? parentInstance } does not have any methods to append a child` ); } }, // asks us if CreateTextInstance must be called for text content of that specific element // for explicit TextRun we return true, so no CreateTextInstance is called shouldSetTextContent(type, props) { return ( type === "textrun" && (typeof props.children === "string" || typeof props.children === "number") ); }, // create implicit TextRun createTextInstance( text, rootContainerInstance, hostContext, internalInstanceHandle ) { return new Docx.TextRun({ text }); }, /// we use that to add sections to document we have in our context /// in this step all instances are created, and we add fictive section instance to document finalizeInitialChildren( domElement, type, _props, rootContainerInstance, hostContext ) { const { children, ...props } = _props; if (domElement.type === "section" && hostContext.isRootContext) { hostContext.document.addSection({ children: domElement.children, properties: props, }); } else if (domElement.type === "section" || hostContext.isRootContext) { throw new Error("Section is not a root element or part of root Fragment"); } }, /// provide document instance to all children getRootHostContext(rootContainerInstance) { return { document: rootContainerInstance.document, isRootContext: true }; }, // this is how we let createInstance know that its a child element getChildHostContext(parentHostContext, type, rootContainerInstance) { return { ...parentHostContext, isRootContext: false, type }; }, // or even that getPublicInstance(instance) { return instance; }, // pre/post commit callbacks prepareForCommit() { return null; }, preparePortalMount() { return null; }, clearContainer() { return false; }, resetAfterCommit(containerInfo) {}, prepareUpdate( domElement, type, oldProps, newProps, rootContainerInstance, hostContext ) { console.log( "prepareUpdate", domElement, type, oldProps, newProps, rootContainerInstance, hostContext ); return [null]; }, shouldDeprioritizeSubtree(type, props) { //console.log("shouldDeprioritizeSubtree", type, props); return false; }, now: Date.now(), isPrimaryRenderer: true, scheduleDeferredCallback: "", cancelDeferredCallback: "", // ------------------- // Mutation // ------------------- supportsMutation: true, commitMount(domElement, type, newProps, internalInstanceHandle) {}, commitUpdate( domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle ) {}, resetTextContent(domElement) {}, commitTextUpdate(textInstance, oldText, newText) {}, appendChild(parentInstance, child) {}, appendChildToContainer(container, child) {}, insertBefore(parentInstance, child, beforeChild) {}, insertInContainerBefore(container, child, beforeChild) {}, removeChild(parentInstance, child) {}, removeChildFromContainer(container, child) {}, }; const DocxRenderer = ReactReconciler(hostConfig); const render = (elements, containerNode, callback) => { if (!containerNode || !is.obj(containerNode)) throw new Error("containerNode must be an empty object"); // We must do this only once if (!containerNode.__internalContainerStructure) { containerNode.__internalContainerStructure = DocxRenderer.createContainer( containerNode, false, false ); } DocxRenderer.updateContainer( elements, containerNode.__internalContainerStructure, null, callback ); }; export const renderAsyncDocument = (elements, options, fileProperties) => { const containerNode = {}; containerNode.document = new Docx.Document(options, fileProperties); return new Promise((resolve) => { render(elements, containerNode, () => resolve(containerNode.document)); }); };