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

Commit

Permalink
Merge pull request #7 from PDFTron/2.1.0
Browse files Browse the repository at this point in the history
2.1.0
  • Loading branch information
S0ulDrag0n authored May 25, 2023
2 parents dd829a4 + 1fb0f6b commit 5aa622e
Show file tree
Hide file tree
Showing 12 changed files with 683 additions and 17 deletions.
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "webviewer",
"widgetName": "WebViewer",
"version": "2.0.0",
"version": "2.1.0",
"description": "My widget description",
"copyright": "2023 Apryse",
"author": "Andrey Safonov",
Expand Down Expand Up @@ -30,7 +30,7 @@
"@types/react-dom": "~18.0.5"
},
"dependencies": {
"@pdftron/webviewer": "^10.0.0-20230412",
"@pdftron/webviewer": "^10.1.0-20230523",
"classnames": "^2.2.6",
"lodash": "^4.17.21"
}
Expand Down
16 changes: 16 additions & 0 deletions src/WebViewer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@
<description>Switches WebViewer to Office editing.</description>
</property>
</propertyGroup>
<propertyGroup caption="Page Extraction">
<property key="enablePageExtraction" type="boolean" defaultValue="false">
<caption>Enable page extraction</caption>
<description>Enable page extraction so that users can select which pages they want to save to another file.</description>
</property>
<property key="allowExtractionDownload" type="boolean" defaultValue="false">
<caption>Allow extraction download</caption>
<description>Allow the extracted pages to be downloaded.</description>
</property>
</propertyGroup>
<propertyGroup caption="Other">
<property key="enableFullAPI" type="boolean" defaultValue="false">
<caption>Enable full API</caption>
Expand Down Expand Up @@ -191,6 +201,12 @@
<description>The interval in milliseconds to get XFDF updates from the server for the current file.</description>
</property>
</propertyGroup>
<propertyGroup caption="Page Extraction">
<property key="allowSavingToMendix" type="boolean" defaultValue="false">
<caption>Allow saving to Mendix</caption>
<description>Allow the extracted pages to be saved back to Mendix as a separate file.</description>
</property>
</propertyGroup>
</propertyGroup>
<propertyGroup caption="License">
<property key="l" type="string" required="false">
Expand Down
39 changes: 38 additions & 1 deletion src/components/PDFViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { createElement, useRef, useEffect, useState } from "react";
import { debounce } from "lodash";
import debounce from "lodash/debounce";
import viewer, { WebViewerInstance } from "@pdftron/webviewer";
import WebViewerModuleClient from "../clients/WebViewerModuleClient";
import PageExtractionModal from "./PageExtractionModal";

export interface InputProps {
containerHeight: string;
Expand Down Expand Up @@ -30,6 +31,9 @@ export interface InputProps {
defaultLanguage: string;
enablePdfEditing?: boolean;
enableOfficeEditing?: boolean;
enablePageExtraction?: boolean;
allowExtractionDownload?: boolean;
allowSavingToMendix?: boolean;
l?: string;
mx: any;
enableDocumentUpdates?: boolean;
Expand Down Expand Up @@ -308,12 +312,45 @@ const PDFViewer: React.FC<InputProps> = props => {
} else {
UI.disableFeatures([UI.Feature.ContentEdit]);
}

// Check whether the backend module is available
moduleClient.checkForModule().then(hasWebViewerModule => {
if (!hasWebViewerModule) {
return;
}

if (props.enablePageExtraction) {
const dataElement = "pageExtractionElement";
UI.addCustomModal({
dataElement,
render: (): any => {
return (
<PageExtractionModal
wvInstance={instance}
dataElement={dataElement}
moduleClient={moduleClientRef.current}
allowDownload={!!props.allowExtractionDownload}
allowSaveAs={!!props.allowSavingToMendix}
/>
);
},
header: undefined,
body: undefined,
footer: undefined
});

UI.setHeaderItems((header: any) => {
header.push({
type: "actionButton",
title: "Page Extraction",
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>`,
onClick: () => {
UI.openElements([dataElement]);
}
});
});
}

UI.setHeaderItems((header: any) => {
if (props.enableDocumentUpdates) {
header.push({
Expand Down
113 changes: 113 additions & 0 deletions src/components/PageExtractionModal/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { createElement } from "react";

interface ListItemInputProps {
item: any;
render: any;
parentAddEventListener: any;
parentRemoveEventListener: any;
}

interface ListItemState {
renderTarget: any;
shouldRenderItem: boolean;
isVisible: boolean;
}

class ListItem extends React.Component<ListItemInputProps, ListItemState> {
private static MAX_SIZE = 0;
private _containerRef: React.RefObject<HTMLDivElement>;
private _measurementRef: React.RefObject<HTMLDivElement>;
private _resizeObserver: ResizeObserver;
private _height = 0;
private _scrollHandle: any;
constructor(props: ListItemInputProps) {
super(props);
this._containerRef = React.createRef();
this._measurementRef = React.createRef();
this._resizeObserver = new ResizeObserver(() => {
const rect = this._measurementRef.current?.getBoundingClientRect();
if (!rect || rect.height === 0 || (this._height && rect.height < this._height)) {
return;
}
this._height = rect.height;
if (this._height > ListItem.MAX_SIZE) {
ListItem.MAX_SIZE = this._height;
}
});
this.props.parentAddEventListener("scroll", this.onParentScroll);
const renderTarget = this.props.render(this.props.item);
const isPromise = renderTarget instanceof Promise;
if (isPromise) {
renderTarget.then((result: any) => this.setState({ renderTarget: result, shouldRenderItem: true }));
}
this.state = {
renderTarget: isPromise ? undefined : renderTarget,
shouldRenderItem: !isPromise,
isVisible: true
};
}
componentDidMount(): void {
// @ts-ignore
this._resizeObserver.observe(this._measurementRef.current);
}
componentWillUnmount(): void {
if (this._measurementRef.current) {
this._resizeObserver.unobserve(this._measurementRef.current);
}
this.props.parentRemoveEventListener("scroll", this.onParentScroll);
}
onParentScroll = (parentRect: any, _scrollTop: number, padding: number) => {
clearTimeout(this._scrollHandle);
this._scrollHandle = setTimeout(() => {
const rect = this._containerRef.current?.getBoundingClientRect();
if (this.doRectanglesIntersect(parentRect, rect, padding)) {
this.setState({ isVisible: true });
} else {
this.setState({ isVisible: false });
}
}, 100);
};
doRectanglesIntersect = (rect1: any, rect2: any, padding = 13): boolean => {
const itemPadding = ListItem.MAX_SIZE * padding;
const rect1Top = rect1.y - itemPadding;
const rect1Bottom = rect1.y + rect1.height + itemPadding;
const rect2Top = rect2.y;
const rect2Bottom = rect2.y + rect2.height;

const verticalIntersection = rect1Top < rect2Bottom && rect1Bottom > rect2Top;

return verticalIntersection;
};
render(): JSX.Element {
if (!this.state.shouldRenderItem) {
return <></>;
}
return (
<div
ref={this._containerRef}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "0.5em 0px",
// DEBUGGING ONLY
// backgroundColor: this.state.isVisible ? "green" : "red"
}}
>
<div ref={this._measurementRef}>
{this.state.isVisible ? (
this.state.renderTarget
) : (
<div
style={{
height: `${ListItem.MAX_SIZE < this._height ? this._height : ListItem.MAX_SIZE}px`
}}
></div>
)}
</div>
</div>
);
}
}

export default ListItem;
117 changes: 117 additions & 0 deletions src/components/PageExtractionModal/PageExtractionThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { createElement } from "react";

interface PageExtractionThumbnailInputProps {
wvInstance: any;
pageNumber: number;
addFileInputEventListener: any;
removeFileInputEventListener: any;
onClick: any;
}

interface PageExtractionThumbnailState {
thumbnail?: string;
isHover: boolean;
isSelected: boolean;
isDisabled: boolean;
}

const ListItemStyle = { display: "inline-block", boxShadow: "1px 1px 8px black", position: "relative" };
const ListItemHoverStyle = { ...ListItemStyle, boxShadow: "1px 1px 5px #3183c8" };

class PageExtractionThumbnail extends React.Component<PageExtractionThumbnailInputProps, PageExtractionThumbnailState> {
constructor(props: PageExtractionThumbnailInputProps) {
super(props);
this.props.wvInstance.Core.documentViewer
.getDocument()
.getDocumentCompletePromise()
.then(() => {
this.props.wvInstance.Core.documentViewer
.getDocument()
.loadThumbnail(this.props.pageNumber, (thumbnailCanvas: HTMLCanvasElement) => {
this.setState({
thumbnail: thumbnailCanvas.toDataURL()
});
});
});
this.state = {
thumbnail: undefined,
isHover: false,
isSelected: this.props.pageNumber === 1,
isDisabled: false
};
}
componentDidMount(): void {
this.props.addFileInputEventListener(this.props.pageNumber, this.onFileInputChanged);
}
componentWillUnmount(): void {
this.props.removeFileInputEventListener(this.props.pageNumber, this.onFileInputChanged);
}
onFileInputChanged = (input: string) => {
const parts = input.split(",").sort();
let occurrances = 0;
let isSelected = false;
for (const part of parts) {
const rangeParts = part.split("-").sort();
const isRange = rangeParts.length === 2;

if (isRange) {
const lower = Number(rangeParts[0]);
const upper = Number(rangeParts[1]);
if (this.props.pageNumber >= lower && this.props.pageNumber <= upper) {
isSelected = true;
occurrances = occurrances ? occurrances++ : 2;
}
} else if (Number(part) === this.props.pageNumber) {
isSelected = Number(rangeParts[0]) === this.props.pageNumber;
occurrances++;
}
}
this.setState({
isSelected,
isDisabled: occurrances > 1
});
};
onHoverEnter = () => {
if (this.state.isDisabled) {
return;
}
this.setState({ isHover: true });
};
onHoverLeave = () => {
if (this.state.isDisabled) {
return;
}
this.setState({ isHover: false });
};
onClick = () => {
if (this.state.isDisabled) {
return;
}
this.props.onClick(this.props.pageNumber, !this.state.isSelected);
this.setState({ isSelected: !this.state.isSelected });
};
render(): JSX.Element {
const { thumbnail, isHover, isSelected } = this.state;
const listItemStyle = isHover ? ListItemHoverStyle : ListItemStyle;
return (
<div
// @ts-ignore
style={listItemStyle}
onMouseEnter={this.onHoverEnter}
onMouseLeave={this.onHoverLeave}
onClick={this.onClick}
>
<img src={thumbnail} />
<input
type="checkbox"
style={{ position: "absolute", top: 0, left: 0 }}
disabled={this.state.isDisabled}
checked={isSelected}
onClick={this.onClick}
/>
</div>
);
}
}

export default PageExtractionThumbnail;
Loading

0 comments on commit 5aa622e

Please sign in to comment.