Skip to content
Open
Show file tree
Hide file tree
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
58 changes: 58 additions & 0 deletions docs/api/functions/createPDFDownload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[dt-utils](../globals.md) / createPDFDownload

# Function: createPDFDownload()

> **createPDFDownload**(`container`, `options`): `HTMLElement` \| `null`

Defined in: [createPDFDownload/index.ts:50](https://github.com/DTStack/dt-utils/blob/master/src/createPDFDownload/index.ts#L50)

创建用于 PDF 下载的 DOM 副本,并移除报告内容中的滚动条。

## Parameters

### container

`HTMLElement` \| `null` \| `undefined`

需要导出的报告根节点

### options

`CreatePDFDownloadOptions` = `{}`

导出 DOM 配置

#### options.width

`number` \| `string`

导出根节点宽度。默认使用原始节点的 `getBoundingClientRect().width`。

#### options.maxWidth

`string`

导出根节点最大宽度。默认移除 `max-width` 限制,避免离屏截图时内容被再次收窄。

## Returns

`HTMLElement` \| `null`

可交给 PDF 导出工具的 DOM 副本

## Description

PDF 导出通常会将页面 DOM 转成图片再写入 PDF。如果直接复用预览区域,内部组件的固定高度和 `overflow: auto/scroll` 会被一起截图,导致导出的 PDF 出现滚动条。

该函数会 clone 一份独立 DOM,展开其中的滚动容器,并保留原始节点宽度。原始 DOM 不会被修改。

## Example

```typescript
import { createPDFDownload } from '@dtinsight/dt-utils';

exportPDF({
filename: '分析报告',
getExportDom: () => createPDFDownload(reportRef.current),
});
```
1 change: 1 addition & 0 deletions docs/api/globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
## Utils

- [copy](functions/copy.md)
- [createPDFDownload](functions/createPDFDownload.md)
- [downloadFile](functions/downloadFile.md)
- [generateUniqueId](functions/generateUniqueId.md)
- [generateUrlWithQuery](functions/generateUrlWithQuery.md)
Expand Down
6 changes: 5 additions & 1 deletion docs/api/typedoc-sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"text": "copy",
"link": "/api/functions/copy.md"
},
{
"text": "createPDFDownload",
"link": "/api/functions/createPDFDownload.md"
},
{
"text": "downloadFile",
"link": "/api/functions/downloadFile.md"
Expand Down Expand Up @@ -149,4 +153,4 @@
}
]
}
]
]
75 changes: 75 additions & 0 deletions src/createPDFDownload/__test__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import createPDFDownload from '../index';

describe('createPDFDownload', () => {
test('return null when container is empty', () => {
expect(createPDFDownload(null)).toBeNull();
expect(createPDFDownload(undefined)).toBeNull();
});

test('clone dom without changing source element', () => {
const container = document.createElement('div');
container.innerHTML = '<section><p>报告内容</p></section>';

const exportDom = createPDFDownload(container);

expect(exportDom).not.toBe(container);
expect(exportDom?.innerHTML).toBe(container.innerHTML);
expect(container.getAttribute('style')).toBeNull();
});

test('expand scroll containers in cloned dom', () => {
const container = document.createElement('div');
container.innerHTML = `
<section class="report-section" style="overflow: auto; height: 120px; max-height: 120px;">
<div class="report-content" style="overflow-y: scroll; height: 80px;">报告内容</div>
</section>
`;

const exportDom = createPDFDownload(container);
const sourceSection = container.querySelector<HTMLElement>('.report-section');
const exportSection = exportDom?.querySelector<HTMLElement>('.report-section');
const exportContent = exportDom?.querySelector<HTMLElement>('.report-content');

expect(sourceSection?.style.overflow).toBe('auto');
expect(sourceSection?.style.height).toBe('120px');
expect(exportSection?.style.height).toBe('auto');
expect(exportSection?.style.maxHeight).toBe('none');
expect(exportSection?.style.overflow).toBe('visible');
expect(exportSection?.style.overflowX).toBe('visible');
expect(exportSection?.style.overflowY).toBe('visible');
expect(exportContent?.style.height).toBe('auto');
expect(exportContent?.style.overflowY).toBe('visible');
});

test('set root width from container rect by default', () => {
const container = document.createElement('div');
jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({
width: 742,
height: 100,
top: 0,
right: 742,
bottom: 100,
left: 0,
x: 0,
y: 0,
toJSON: jest.fn(),
});

const exportDom = createPDFDownload(container);

expect(exportDom?.style.width).toBe('742px');
expect(exportDom?.style.maxWidth).toBe('none');
});

test('support custom root width and max width', () => {
const container = document.createElement('div');

const exportDom = createPDFDownload(container, {
width: '100%',
maxWidth: '960px',
});

expect(exportDom?.style.width).toBe('100%');
expect(exportDom?.style.maxWidth).toBe('960px');
});
});
84 changes: 84 additions & 0 deletions src/createPDFDownload/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
export interface CreatePDFDownloadOptions {
/**
* 导出根节点宽度。默认使用原始节点的 getBoundingClientRect().width。
*/
width?: number | string;
/**
* 导出根节点最大宽度。默认移除 max-width 限制,避免离屏截图时内容被再次收窄。
*/
maxWidth?: string;
}

const SCROLL_OVERFLOW_REGEXP = /auto|scroll/;

function isScrollContainer(element: HTMLElement) {
const { overflow, overflowX, overflowY } = window.getComputedStyle(element);
return [overflow, overflowX, overflowY].some((value) => SCROLL_OVERFLOW_REGEXP.test(value));
}

function expandScrollContainer(element: HTMLElement) {
element.style.setProperty('height', 'auto', 'important');
element.style.setProperty('max-height', 'none', 'important');
element.style.setProperty('overflow', 'visible', 'important');
element.style.setProperty('overflow-x', 'visible', 'important');
element.style.setProperty('overflow-y', 'visible', 'important');
}

/**
* 创建用于 PDF 下载的 DOM 副本,并移除报告内容中的滚动条。
*
* @category Utils
* @description
* PDF 导出通常会将页面 DOM 转成图片再写入 PDF。如果直接复用预览区域,
* 内部组件的固定高度和 `overflow: auto/scroll` 会被一起截图,导致导出的 PDF
* 出现滚动条。该函数会 clone 一份独立 DOM,展开其中的滚动容器,并保留原始节点宽度。
*
* @param {HTMLElement | null | undefined} container - 需要导出的报告根节点
* @param {CreatePDFDownloadOptions} options - 导出 DOM 配置
* @returns {HTMLElement | null} 可交给 PDF 导出工具的 DOM 副本
*
* @example
* ```typescript
* import { createPDFDownload } from '@dtinsight/dt-utils';
*
* exportPDF({
* filename: '分析报告',
* getExportDom: () => createPDFDownload(reportRef.current),
* });
* ```
*/
const createPDFDownload = (
container?: HTMLElement | null,
options: CreatePDFDownloadOptions = {}
): HTMLElement | null => {
if (!container) return null;

const exportDom = container.cloneNode(true) as HTMLElement;
const sourceElements = [container].concat(
Array.from(container.querySelectorAll<HTMLElement>('*'))
);
const exportElements = [exportDom].concat(
Array.from(exportDom.querySelectorAll<HTMLElement>('*'))
);

sourceElements.forEach((sourceElement, index) => {
const exportElement = exportElements[index];
if (!exportElement || !isScrollContainer(sourceElement)) return;

expandScrollContainer(exportElement);
});

const width =
options.width === undefined ? container.getBoundingClientRect().width : options.width;
if (typeof width === 'number') {
exportDom.style.width = `${width}px`;
} else if (width) {
exportDom.style.width = width;
}

exportDom.style.maxWidth = options.maxWidth || 'none';

return exportDom;
};

export default createPDFDownload;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * as lodash from 'lodash-es';
export { default as checkBrowserSupport } from './checkBrowserSupport';
export { default as Cookies } from './cookies';
export { default as copy } from './copy';
export { default as createPDFDownload } from './createPDFDownload';
export { default as downloadFile } from './downloadFile';
export { default as formatBytes } from './formatBytes';
export { DateTimeFormat, default as formatDateTime } from './formatDateTime';
Expand Down
Loading