Skip to content
This repository was archived by the owner on Feb 19, 2025. It is now read-only.

Commit 5aa622e

Browse files
authored
Merge pull request #7 from PDFTron/2.1.0
2.1.0
2 parents dd829a4 + 1fb0f6b commit 5aa622e

File tree

12 files changed

+683
-17
lines changed

12 files changed

+683
-17
lines changed

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "webviewer",
33
"widgetName": "WebViewer",
4-
"version": "2.0.0",
4+
"version": "2.1.0",
55
"description": "My widget description",
66
"copyright": "2023 Apryse",
77
"author": "Andrey Safonov",
@@ -30,7 +30,7 @@
3030
"@types/react-dom": "~18.0.5"
3131
},
3232
"dependencies": {
33-
"@pdftron/webviewer": "^10.0.0-20230412",
33+
"@pdftron/webviewer": "^10.1.0-20230523",
3434
"classnames": "^2.2.6",
3535
"lodash": "^4.17.21"
3636
}

src/WebViewer.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@
3636
<description>Switches WebViewer to Office editing.</description>
3737
</property>
3838
</propertyGroup>
39+
<propertyGroup caption="Page Extraction">
40+
<property key="enablePageExtraction" type="boolean" defaultValue="false">
41+
<caption>Enable page extraction</caption>
42+
<description>Enable page extraction so that users can select which pages they want to save to another file.</description>
43+
</property>
44+
<property key="allowExtractionDownload" type="boolean" defaultValue="false">
45+
<caption>Allow extraction download</caption>
46+
<description>Allow the extracted pages to be downloaded.</description>
47+
</property>
48+
</propertyGroup>
3949
<propertyGroup caption="Other">
4050
<property key="enableFullAPI" type="boolean" defaultValue="false">
4151
<caption>Enable full API</caption>
@@ -191,6 +201,12 @@
191201
<description>The interval in milliseconds to get XFDF updates from the server for the current file.</description>
192202
</property>
193203
</propertyGroup>
204+
<propertyGroup caption="Page Extraction">
205+
<property key="allowSavingToMendix" type="boolean" defaultValue="false">
206+
<caption>Allow saving to Mendix</caption>
207+
<description>Allow the extracted pages to be saved back to Mendix as a separate file.</description>
208+
</property>
209+
</propertyGroup>
194210
</propertyGroup>
195211
<propertyGroup caption="License">
196212
<property key="l" type="string" required="false">

src/components/PDFViewer.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React, { createElement, useRef, useEffect, useState } from "react";
2-
import { debounce } from "lodash";
2+
import debounce from "lodash/debounce";
33
import viewer, { WebViewerInstance } from "@pdftron/webviewer";
44
import WebViewerModuleClient from "../clients/WebViewerModuleClient";
5+
import PageExtractionModal from "./PageExtractionModal";
56

67
export interface InputProps {
78
containerHeight: string;
@@ -30,6 +31,9 @@ export interface InputProps {
3031
defaultLanguage: string;
3132
enablePdfEditing?: boolean;
3233
enableOfficeEditing?: boolean;
34+
enablePageExtraction?: boolean;
35+
allowExtractionDownload?: boolean;
36+
allowSavingToMendix?: boolean;
3337
l?: string;
3438
mx: any;
3539
enableDocumentUpdates?: boolean;
@@ -308,12 +312,45 @@ const PDFViewer: React.FC<InputProps> = props => {
308312
} else {
309313
UI.disableFeatures([UI.Feature.ContentEdit]);
310314
}
315+
311316
// Check whether the backend module is available
312317
moduleClient.checkForModule().then(hasWebViewerModule => {
313318
if (!hasWebViewerModule) {
314319
return;
315320
}
316321

322+
if (props.enablePageExtraction) {
323+
const dataElement = "pageExtractionElement";
324+
UI.addCustomModal({
325+
dataElement,
326+
render: (): any => {
327+
return (
328+
<PageExtractionModal
329+
wvInstance={instance}
330+
dataElement={dataElement}
331+
moduleClient={moduleClientRef.current}
332+
allowDownload={!!props.allowExtractionDownload}
333+
allowSaveAs={!!props.allowSavingToMendix}
334+
/>
335+
);
336+
},
337+
header: undefined,
338+
body: undefined,
339+
footer: undefined
340+
});
341+
342+
UI.setHeaderItems((header: any) => {
343+
header.push({
344+
type: "actionButton",
345+
title: "Page Extraction",
346+
img: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path class="cls-1" d="M16.49,13.54h1.83V9.25s0,0,0-.06a.59.59,0,0,0,0-.23.32.32,0,0,0,0-.09.8.8,0,0,0-.18-.27l-5.5-5.5a.93.93,0,0,0-.26-.18l-.09,0a1,1,0,0,0-.24,0l-.05,0H5.49A1.84,1.84,0,0,0,3.66,4.67V19.33a1.84,1.84,0,0,0,1.83,1.84H11V19.33H5.49V4.67H11V9.25a.92.92,0,0,0,.92.92h4.58Z"/><path class="cls-1" d="M20.21,17.53,17.05,15a.37.37,0,0,0-.6.29v1.6H12.78v1.84h3.67v1.61a.37.37,0,0,0,.6.29l3.16-2.53A.37.37,0,0,0,20.21,17.53Z"/></svg>`,
347+
onClick: () => {
348+
UI.openElements([dataElement]);
349+
}
350+
});
351+
});
352+
}
353+
317354
UI.setHeaderItems((header: any) => {
318355
if (props.enableDocumentUpdates) {
319356
header.push({
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React, { createElement } from "react";
2+
3+
interface ListItemInputProps {
4+
item: any;
5+
render: any;
6+
parentAddEventListener: any;
7+
parentRemoveEventListener: any;
8+
}
9+
10+
interface ListItemState {
11+
renderTarget: any;
12+
shouldRenderItem: boolean;
13+
isVisible: boolean;
14+
}
15+
16+
class ListItem extends React.Component<ListItemInputProps, ListItemState> {
17+
private static MAX_SIZE = 0;
18+
private _containerRef: React.RefObject<HTMLDivElement>;
19+
private _measurementRef: React.RefObject<HTMLDivElement>;
20+
private _resizeObserver: ResizeObserver;
21+
private _height = 0;
22+
private _scrollHandle: any;
23+
constructor(props: ListItemInputProps) {
24+
super(props);
25+
this._containerRef = React.createRef();
26+
this._measurementRef = React.createRef();
27+
this._resizeObserver = new ResizeObserver(() => {
28+
const rect = this._measurementRef.current?.getBoundingClientRect();
29+
if (!rect || rect.height === 0 || (this._height && rect.height < this._height)) {
30+
return;
31+
}
32+
this._height = rect.height;
33+
if (this._height > ListItem.MAX_SIZE) {
34+
ListItem.MAX_SIZE = this._height;
35+
}
36+
});
37+
this.props.parentAddEventListener("scroll", this.onParentScroll);
38+
const renderTarget = this.props.render(this.props.item);
39+
const isPromise = renderTarget instanceof Promise;
40+
if (isPromise) {
41+
renderTarget.then((result: any) => this.setState({ renderTarget: result, shouldRenderItem: true }));
42+
}
43+
this.state = {
44+
renderTarget: isPromise ? undefined : renderTarget,
45+
shouldRenderItem: !isPromise,
46+
isVisible: true
47+
};
48+
}
49+
componentDidMount(): void {
50+
// @ts-ignore
51+
this._resizeObserver.observe(this._measurementRef.current);
52+
}
53+
componentWillUnmount(): void {
54+
if (this._measurementRef.current) {
55+
this._resizeObserver.unobserve(this._measurementRef.current);
56+
}
57+
this.props.parentRemoveEventListener("scroll", this.onParentScroll);
58+
}
59+
onParentScroll = (parentRect: any, _scrollTop: number, padding: number) => {
60+
clearTimeout(this._scrollHandle);
61+
this._scrollHandle = setTimeout(() => {
62+
const rect = this._containerRef.current?.getBoundingClientRect();
63+
if (this.doRectanglesIntersect(parentRect, rect, padding)) {
64+
this.setState({ isVisible: true });
65+
} else {
66+
this.setState({ isVisible: false });
67+
}
68+
}, 100);
69+
};
70+
doRectanglesIntersect = (rect1: any, rect2: any, padding = 13): boolean => {
71+
const itemPadding = ListItem.MAX_SIZE * padding;
72+
const rect1Top = rect1.y - itemPadding;
73+
const rect1Bottom = rect1.y + rect1.height + itemPadding;
74+
const rect2Top = rect2.y;
75+
const rect2Bottom = rect2.y + rect2.height;
76+
77+
const verticalIntersection = rect1Top < rect2Bottom && rect1Bottom > rect2Top;
78+
79+
return verticalIntersection;
80+
};
81+
render(): JSX.Element {
82+
if (!this.state.shouldRenderItem) {
83+
return <></>;
84+
}
85+
return (
86+
<div
87+
ref={this._containerRef}
88+
style={{
89+
display: "flex",
90+
alignItems: "center",
91+
justifyContent: "center",
92+
padding: "0.5em 0px",
93+
// DEBUGGING ONLY
94+
// backgroundColor: this.state.isVisible ? "green" : "red"
95+
}}
96+
>
97+
<div ref={this._measurementRef}>
98+
{this.state.isVisible ? (
99+
this.state.renderTarget
100+
) : (
101+
<div
102+
style={{
103+
height: `${ListItem.MAX_SIZE < this._height ? this._height : ListItem.MAX_SIZE}px`
104+
}}
105+
></div>
106+
)}
107+
</div>
108+
</div>
109+
);
110+
}
111+
}
112+
113+
export default ListItem;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import React, { createElement } from "react";
2+
3+
interface PageExtractionThumbnailInputProps {
4+
wvInstance: any;
5+
pageNumber: number;
6+
addFileInputEventListener: any;
7+
removeFileInputEventListener: any;
8+
onClick: any;
9+
}
10+
11+
interface PageExtractionThumbnailState {
12+
thumbnail?: string;
13+
isHover: boolean;
14+
isSelected: boolean;
15+
isDisabled: boolean;
16+
}
17+
18+
const ListItemStyle = { display: "inline-block", boxShadow: "1px 1px 8px black", position: "relative" };
19+
const ListItemHoverStyle = { ...ListItemStyle, boxShadow: "1px 1px 5px #3183c8" };
20+
21+
class PageExtractionThumbnail extends React.Component<PageExtractionThumbnailInputProps, PageExtractionThumbnailState> {
22+
constructor(props: PageExtractionThumbnailInputProps) {
23+
super(props);
24+
this.props.wvInstance.Core.documentViewer
25+
.getDocument()
26+
.getDocumentCompletePromise()
27+
.then(() => {
28+
this.props.wvInstance.Core.documentViewer
29+
.getDocument()
30+
.loadThumbnail(this.props.pageNumber, (thumbnailCanvas: HTMLCanvasElement) => {
31+
this.setState({
32+
thumbnail: thumbnailCanvas.toDataURL()
33+
});
34+
});
35+
});
36+
this.state = {
37+
thumbnail: undefined,
38+
isHover: false,
39+
isSelected: this.props.pageNumber === 1,
40+
isDisabled: false
41+
};
42+
}
43+
componentDidMount(): void {
44+
this.props.addFileInputEventListener(this.props.pageNumber, this.onFileInputChanged);
45+
}
46+
componentWillUnmount(): void {
47+
this.props.removeFileInputEventListener(this.props.pageNumber, this.onFileInputChanged);
48+
}
49+
onFileInputChanged = (input: string) => {
50+
const parts = input.split(",").sort();
51+
let occurrances = 0;
52+
let isSelected = false;
53+
for (const part of parts) {
54+
const rangeParts = part.split("-").sort();
55+
const isRange = rangeParts.length === 2;
56+
57+
if (isRange) {
58+
const lower = Number(rangeParts[0]);
59+
const upper = Number(rangeParts[1]);
60+
if (this.props.pageNumber >= lower && this.props.pageNumber <= upper) {
61+
isSelected = true;
62+
occurrances = occurrances ? occurrances++ : 2;
63+
}
64+
} else if (Number(part) === this.props.pageNumber) {
65+
isSelected = Number(rangeParts[0]) === this.props.pageNumber;
66+
occurrances++;
67+
}
68+
}
69+
this.setState({
70+
isSelected,
71+
isDisabled: occurrances > 1
72+
});
73+
};
74+
onHoverEnter = () => {
75+
if (this.state.isDisabled) {
76+
return;
77+
}
78+
this.setState({ isHover: true });
79+
};
80+
onHoverLeave = () => {
81+
if (this.state.isDisabled) {
82+
return;
83+
}
84+
this.setState({ isHover: false });
85+
};
86+
onClick = () => {
87+
if (this.state.isDisabled) {
88+
return;
89+
}
90+
this.props.onClick(this.props.pageNumber, !this.state.isSelected);
91+
this.setState({ isSelected: !this.state.isSelected });
92+
};
93+
render(): JSX.Element {
94+
const { thumbnail, isHover, isSelected } = this.state;
95+
const listItemStyle = isHover ? ListItemHoverStyle : ListItemStyle;
96+
return (
97+
<div
98+
// @ts-ignore
99+
style={listItemStyle}
100+
onMouseEnter={this.onHoverEnter}
101+
onMouseLeave={this.onHoverLeave}
102+
onClick={this.onClick}
103+
>
104+
<img src={thumbnail} />
105+
<input
106+
type="checkbox"
107+
style={{ position: "absolute", top: 0, left: 0 }}
108+
disabled={this.state.isDisabled}
109+
checked={isSelected}
110+
onClick={this.onClick}
111+
/>
112+
</div>
113+
);
114+
}
115+
}
116+
117+
export default PageExtractionThumbnail;

0 commit comments

Comments
 (0)