UNPKG

@mui/x-data-grid

Version:

The community edition of the data grid component (MUI X).

274 lines (262 loc) 12.2 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import { unstable_ownerDocument as ownerDocument } from '@mui/utils'; import { useGridLogger } from '../../utils/useGridLogger'; import { gridExpandedRowCountSelector } from '../filter/gridFilterSelector'; import { gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector } from '../columns/gridColumnsSelector'; import { gridClasses } from '../../../constants/gridClasses'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector'; import { getColumnsToExport } from './utils'; import { mergeStateWithPaginationModel } from '../pagination/useGridPagination'; import { useGridRegisterPipeProcessor } from '../../core/pipeProcessing'; import { GridPrintExportMenuItem } from '../../../components/toolbar/GridToolbarExport'; import { getTotalHeaderHeight } from '../columns/gridColumnsUtils'; import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../../../colDef/gridCheckboxSelectionColDef'; import { jsx as _jsx } from "react/jsx-runtime"; function raf() { return new Promise(resolve => { requestAnimationFrame(() => { resolve(); }); }); } function buildPrintWindow(title) { const iframeEl = document.createElement('iframe'); iframeEl.style.position = 'absolute'; iframeEl.style.width = '0px'; iframeEl.style.height = '0px'; iframeEl.title = title || document.title; return iframeEl; } /** * @requires useGridColumns (state) * @requires useGridFilter (state) * @requires useGridSorting (state) * @requires useGridParamsApi (method) */ export const useGridPrintExport = (apiRef, props) => { const logger = useGridLogger(apiRef, 'useGridPrintExport'); const doc = React.useRef(null); const previousGridState = React.useRef(null); const previousColumnVisibility = React.useRef({}); const previousRows = React.useRef([]); React.useEffect(() => { doc.current = ownerDocument(apiRef.current.rootElementRef.current); }, [apiRef]); // Returns a promise because updateColumns triggers state update and // the new state needs to be in place before the grid can be sized correctly const updateGridColumnsForPrint = React.useCallback((fields, allColumns, includeCheckboxes) => new Promise(resolve => { const exportedColumnFields = getColumnsToExport({ apiRef, options: { fields, allColumns } }).map(column => column.field); const columns = gridColumnDefinitionsSelector(apiRef); const newColumnVisibilityModel = {}; columns.forEach(column => { newColumnVisibilityModel[column.field] = exportedColumnFields.includes(column.field); }); if (includeCheckboxes) { newColumnVisibilityModel[GRID_CHECKBOX_SELECTION_COL_DEF.field] = true; } apiRef.current.setColumnVisibilityModel(newColumnVisibilityModel); resolve(); }), [apiRef]); const updateGridRowsForPrint = React.useCallback(getRowsToExport => { const rowsToExportIds = getRowsToExport({ apiRef }); const newRows = rowsToExportIds.map(id => apiRef.current.getRow(id)); apiRef.current.setRows(newRows); }, [apiRef]); const handlePrintWindowLoad = React.useCallback((printWindow, options) => { const normalizeOptions = _extends({ copyStyles: true, hideToolbar: false, hideFooter: false, includeCheckboxes: false }, options); const printDoc = printWindow.contentDocument; if (!printDoc) { return; } const rowsMeta = gridRowsMetaSelector(apiRef.current.state); const gridRootElement = apiRef.current.rootElementRef.current; const gridClone = gridRootElement.cloneNode(true); // Allow to overflow to not hide the border of the last row const gridMain = gridClone.querySelector(`.${gridClasses.main}`); gridMain.style.overflow = 'visible'; // See https://support.google.com/chrome/thread/191619088?hl=en&msgid=193009642 gridClone.style.contain = 'size'; const columnHeaders = gridClone.querySelector(`.${gridClasses.columnHeaders}`); const columnHeadersInner = columnHeaders.querySelector(`.${gridClasses.columnHeadersInner}`); columnHeadersInner.style.width = '100%'; let gridToolbarElementHeight = gridRootElement.querySelector(`.${gridClasses.toolbarContainer}`)?.offsetHeight || 0; let gridFooterElementHeight = gridRootElement.querySelector(`.${gridClasses.footerContainer}`)?.offsetHeight || 0; if (normalizeOptions.hideToolbar) { gridClone.querySelector(`.${gridClasses.toolbarContainer}`)?.remove(); gridToolbarElementHeight = 0; } if (normalizeOptions.hideFooter) { gridClone.querySelector(`.${gridClasses.footerContainer}`)?.remove(); gridFooterElementHeight = 0; } // Expand container height to accommodate all rows const computedTotalHeight = rowsMeta.currentPageTotalHeight + getTotalHeaderHeight(apiRef, props.columnHeaderHeight) + gridToolbarElementHeight + gridFooterElementHeight; gridClone.style.height = `${computedTotalHeight}px`; // The height above does not include grid border width, so we need to exclude it gridClone.style.boxSizing = 'content-box'; // the footer is always being placed at the bottom of the page as if all rows are exported // so if getRowsToExport is being used to only export a subset of rows then we need to // adjust the footer position to be correctly placed at the bottom of the grid if (options?.getRowsToExport) { const gridFooterElement = gridClone.querySelector(`.${gridClasses.footerContainer}`); gridFooterElement.style.position = 'absolute'; gridFooterElement.style.width = '100%'; gridFooterElement.style.top = `${computedTotalHeight - gridFooterElementHeight}px`; } // printDoc.body.appendChild(gridClone); should be enough but a clone isolation bug in Safari // prevents us to do it const container = document.createElement('div'); container.appendChild(gridClone); printDoc.body.innerHTML = container.innerHTML; const defaultPageStyle = typeof normalizeOptions.pageStyle === 'function' ? normalizeOptions.pageStyle() : normalizeOptions.pageStyle; if (typeof defaultPageStyle === 'string') { // TODO custom styles should always win const styleElement = printDoc.createElement('style'); styleElement.appendChild(printDoc.createTextNode(defaultPageStyle)); printDoc.head.appendChild(styleElement); } if (normalizeOptions.bodyClassName) { printDoc.body.classList.add(...normalizeOptions.bodyClassName.split(' ')); } const stylesheetLoadPromises = []; if (normalizeOptions.copyStyles) { const rootCandidate = gridRootElement.getRootNode(); const root = rootCandidate.constructor.name === 'ShadowRoot' ? rootCandidate : doc.current; const headStyleElements = root.querySelectorAll("style, link[rel='stylesheet']"); for (let i = 0; i < headStyleElements.length; i += 1) { const node = headStyleElements[i]; if (node.tagName === 'STYLE') { const newHeadStyleElements = printDoc.createElement(node.tagName); const sheet = node.sheet; if (sheet) { let styleCSS = ''; // NOTE: for-of is not supported by IE for (let j = 0; j < sheet.cssRules.length; j += 1) { if (typeof sheet.cssRules[j].cssText === 'string') { styleCSS += `${sheet.cssRules[j].cssText}\r\n`; } } newHeadStyleElements.appendChild(printDoc.createTextNode(styleCSS)); printDoc.head.appendChild(newHeadStyleElements); } } else if (node.getAttribute('href')) { // If `href` tag is empty, avoid loading these links const newHeadStyleElements = printDoc.createElement(node.tagName); for (let j = 0; j < node.attributes.length; j += 1) { const attr = node.attributes[j]; if (attr) { newHeadStyleElements.setAttribute(attr.nodeName, attr.nodeValue || ''); } } stylesheetLoadPromises.push(new Promise(resolve => { newHeadStyleElements.addEventListener('load', () => resolve()); })); printDoc.head.appendChild(newHeadStyleElements); } } } // Trigger print if (process.env.NODE_ENV !== 'test') { // wait for remote stylesheets to load Promise.all(stylesheetLoadPromises).then(() => { printWindow.contentWindow.print(); }); } }, [apiRef, doc, props.columnHeaderHeight]); const handlePrintWindowAfterPrint = React.useCallback(printWindow => { // Remove the print iframe doc.current.body.removeChild(printWindow); // Revert grid to previous state apiRef.current.restoreState(previousGridState.current || {}); if (!previousGridState.current?.columns?.columnVisibilityModel) { // if the apiRef.current.exportState(); did not exported the column visibility, we update it apiRef.current.setColumnVisibilityModel(previousColumnVisibility.current); } apiRef.current.unstable_setVirtualization(true); apiRef.current.setRows(previousRows.current); // Clear local state previousGridState.current = null; previousColumnVisibility.current = {}; previousRows.current = []; }, [apiRef]); const exportDataAsPrint = React.useCallback(async options => { logger.debug(`Export data as Print`); if (!apiRef.current.rootElementRef.current) { throw new Error('MUI: No grid root element available.'); } previousGridState.current = apiRef.current.exportState(); // It appends that the visibility model is not exported, especially if columnVisibility is not controlled previousColumnVisibility.current = gridColumnVisibilityModelSelector(apiRef); previousRows.current = apiRef.current.getSortedRows(); if (props.pagination) { const visibleRowCount = gridExpandedRowCountSelector(apiRef); const paginationModel = { page: 0, pageSize: visibleRowCount }; apiRef.current.updateControlState('pagination', // Using signature `DataGridPro` to allow more than 100 rows in the print export mergeStateWithPaginationModel(visibleRowCount, 'DataGridPro', paginationModel)); apiRef.current.forceUpdate(); } await updateGridColumnsForPrint(options?.fields, options?.allColumns, options?.includeCheckboxes); if (options?.getRowsToExport) { updateGridRowsForPrint(options.getRowsToExport); } apiRef.current.unstable_setVirtualization(false); await raf(); // wait for the state changes to take action const printWindow = buildPrintWindow(options?.fileName); if (process.env.NODE_ENV === 'test') { doc.current.body.appendChild(printWindow); // In test env, run the all pipeline without waiting for loading handlePrintWindowLoad(printWindow, options); handlePrintWindowAfterPrint(printWindow); } else { printWindow.onload = () => { handlePrintWindowLoad(printWindow, options); const mediaQueryList = printWindow.contentWindow.matchMedia('print'); mediaQueryList.addEventListener('change', mql => { const isAfterPrint = mql.matches === false; if (isAfterPrint) { handlePrintWindowAfterPrint(printWindow); } }); }; doc.current.body.appendChild(printWindow); } }, [props, logger, apiRef, handlePrintWindowLoad, handlePrintWindowAfterPrint, updateGridColumnsForPrint, updateGridRowsForPrint]); const printExportApi = { exportDataAsPrint }; useGridApiMethod(apiRef, printExportApi, 'public'); /** * PRE-PROCESSING */ const addExportMenuButtons = React.useCallback((initialValue, options) => { if (options.printOptions?.disableToolbarButton) { return initialValue; } return [...initialValue, { component: /*#__PURE__*/_jsx(GridPrintExportMenuItem, { options: options.printOptions }), componentName: 'printExport' }]; }, []); useGridRegisterPipeProcessor(apiRef, 'exportMenu', addExportMenuButtons); };