-
Notifications
You must be signed in to change notification settings - Fork 95
Contribution Guide (Internal)
- Setup: Use an outer repo for testing and an inner repo for development.
- Coding: Use the TypeScript (not JavaScript) files.
-
Testing: Run
npm start
in the outer repo'sexample
folder and proceed with testing. -
Making a New Branch and Pull Request: Create PRs to
master
branch. Every time you update your PR, GitHub Actions will update JS files and commit its changes to the PR. Every PR must have 1 approval before merge (unless you are an administrator). - Upgrading: upgrade example app by following the react-native documentation for upgrades.
- Clone the repo. This is your "outer repo" and is used for testing the app.
- In
example
folder, runnpm install
. For now, if you get an error like "unable to resolve dependency tree", switch tonpm
version 6.14.15. - In
example/node_modules/
, deletereact-native-pdftron
. - In
example/node_modules/
, clone the repo a second time and rename it toreact-native-pdftron
. This is the "inner repo", used for development. - In
example/node_modules/react-native-pdftron
, runnpm install
. - Run
npm install -g typescript
to install the TypeScript compiler globally. - If the testing environment fails - instead of running
npm install
, runyarn install
on all those above steps
In order to provide TypeScript support for users, the JavaScript files have been migrated to TypeScript. The files now use .ts
or .tsx
file extension. For example, DocumentView.js
became DocumentView.tsx
.
You may notice red squiggly lines and warnings/errors while coding. These are TypeScript type checking errors, not syntax errors. Correct them if you can; otherwise, ignore them by commenting // @ts-ignore
.
Step-by-step:
- Config constants
- AnnotOptions type or interface
- DocumentView prop
- DocumentView event listener
- DocumentView method
- PdftronFlutter method
- PDFViewCtrl
- Open the
src/Config/Config.ts
file. - Add your constant(s) to the
Config
object. - If you created a whole new
Config
category (likeButtons
orTools
), scroll down to theConfig
module and addexport type NewCategory = ValueOf<typeof Config.NewCategory>;
AnnotOptions
is used to store reusable object types. These types can be useful in DocumentView
methods and event listeners.
- Open the
src/AnnotOptions/AnnotOptions.ts
file. - Add a new type or interface to represent the object.
- Open the
src/DocumentView/DocumentView.tsx
file. - Add your prop to the
propTypes
object.- If the prop type is “a Config.* constant”, use the
oneOf<>()
helper method. - If the prop type is “an array of Config.* constants”, use the
arrayOf<>()
helper method. - Otherwise, use the standard
PropTypes
types.
- If the prop type is “a Config.* constant”, use the
- Open the
src/DocumentView/DocumentView.tsx
file. - Add your event listener to the
propTypes
object and use thefunc<>()
helper method.- To represent objects that may show up more than once, like annotations or rects, use
AnnotOptions
.
- To represent objects that may show up more than once, like annotations or rects, use
- Add an
if/else
case for your event listener toDocumentView.onChange
method.
- Open the
src/DocumentView/DocumentView.tsx
file. - Add your method to the
DocumentView
class.- The return type should always look like
Promise<void | T>
whereT
is the expected return type upon success. If your method returns void, usePromise<void>
.
- The return type should always look like
- Open the
index.ts
file. - Add the method to the
Pdftron
interface.
- Open the
src/PDFViewCtrl/PDFViewCtrl.tsx
file. - Follow the same steps used for elements in
DocumentView
.
- First, generate JavaScript files from the TypeScript ones:
- In the outer repo's
example
folder, runnpm start
,npm run android
, ornpm run ios
. - OR go to the inner repo and run
tsc
(ornpx tsc
if you have not installedtsc
globally).
- In the outer repo's
- Proceed with testing in
App.js
.- If you would like to observe the effects of TypeScript support from the user's perspective, see Usage Example.
- Create a branch from
master
branch. - Update the source code (in
*.ts
files). - Commit and push the changes with descriptive messages.
- Create a pull request to
master
branch.
Note:
- Every time you open/push to/reopen a PR, GitHub Actions will automatically update JS files and push the update to your PR. These changes will not affect the source code (TS files).
- Every PR needs 1 approving review to be merged into
master
(unless you are an administrator of the repo). - In the rare case that you want to create a PR from your fork to this original repo, the script will not succeed for safety reasons. Instead, the JS files will have to be updated manually. Alternatively, you could transfer your changes to a branch on the original repo and create the PR from here.
Q: What if I created a branch from master before the TypeScript update? Will there be lots of conflicts if I update from master after the TypeScript update?
A: It is unlikely that you will face lots of conflicts. There may be a few in the DocumentView
file because some of its structure has changed. The most likely conflict is because the propTypes
object has been moved from inside DocumentView
class to outside. If you encounter this conflict, make note of what prop you have added, accept the move, and re-add the prop after the move.
If the conflicts are more severe, create a new branch from master
and copy your changes to it.
You can copy the code snippet below into an App.tsx
to test the tooling for the new library. Note that this file cannot be used to build an app unless a TypeScript project is created:
import React, { Component } from 'react';
import {
Platform,
StyleSheet,
Text,
View,
PermissionsAndroid,
BackHandler,
NativeModules,
Alert,
Button
} from 'react-native';
import { AnnotOptions, Config, DocumentView, RNPdftron} from 'react-native-pdftron';
type Props = {};
type State = {permissionGranted: boolean};
let _viewer : DocumentView | null;
export default class App extends Component<Props, State> {
constructor(props: Props) {
super(props);
// Uses the platform to determine if storage permisions have been automatically granted.
// The result of this check is placed in the component's state.
this.state = {
permissionGranted: Platform.OS === 'ios' ? true : false
};
RNPdftron.initialize("Insert commercial license key here after purchase");
RNPdftron.enableJavaScript(true);
}
// Uses the platform to determine if storage permissions need to be requested.
componentDidMount() {
if (Platform.OS === 'android') {
this.requestStoragePermission();
}
}
// Requests storage permissions for Android and updates the component's state using
// the result.
async requestStoragePermission() {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
this.setState({
permissionGranted: true
});
console.log("Storage permission granted");
} else {
this.setState({
permissionGranted: false
});
console.log("Storage permission denied");
}
} catch (err) {
console.warn(err);
}
}
onLeadingNavButtonPressed = () => {
if (_viewer != null) {
_viewer.getPageNumberFromScreenPoint(0, 5894).then((page : number | void) => {
if (typeof page === "number") {
console.log("Page associated with coordinates: ", page);
}
});
}
}
onDocumentLoaded = (path: string) => {
if (_viewer != null) {
_viewer.importAnnotationCommand(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
" <xfdf xmlns=\"http://ns.adobe.com/xfdf/\" xml:space=\"preserve\">\n" +
" <add>\n" +
" <square style=\"solid\" width=\"5\" color=\"#E44234\" opacity=\"1\" creationdate=\"D:20200619203211Z\" flags=\"print\" date=\"D:20200619203211Z\" name=\"c684da06-12d2-4ccd-9361-0a1bf2e089e3\" page=\"1\" rect=\"113.312,277.056,235.43,350.173\" title=\"\" />\n" +
" </add>\n" +
" <modify />\n" +
" <delete />\n" +
" <pdf-info import-version=\"3\" version=\"2\" xmlns=\"http://www.pdftron.com/pdfinfo\" />\n" +
" </xfdf>"
).catch((e) => console.warn(e));
}
}
onAnnotationMenuPress = (event: {annotationMenu:string, annotations: Array<AnnotOptions.Annotation>}) => {
console.log("Menu item pressed: ", event.annotationMenu);
event.annotations.forEach((annot) => {
console.log("Annotation changed with Annotation Menu: ");
console.log("Rect:\n", annot.pageRect, "\n", annot.screenRect);
});
}
onAnnotationSelected = (event: {annotations: Array<AnnotOptions.Annotation>}) => {
event.annotations.forEach((annot) => {
console.log("Selected Annotation:\n");
if (_viewer != null && typeof annot.screenRect === "object") {
_viewer.getAnnotationListAt(annot.screenRect.x1, annot.screenRect.y1, annot.screenRect.x2, annot.screenRect.y2)
.then((annotations) => {
if (Array.isArray(annotations)) {
annotations.forEach((annotation: AnnotOptions.Annotation) => {
console.log("Annotation from list: ",annotation);
})
}
});
_viewer.getAnnotationAtPoint(annot.screenRect.x1, annot.screenRect.y1, 57, 12)
.then((anno) => {
if (typeof anno === "object") {
console.log("Annotation at Point: ", anno);
}
});
}
});
}
onAnnotationChanged = (event: {action: string, annotations: Array<AnnotOptions.Annotation>}) => {
event.annotations.forEach((annot) => {
console.log("Changed Annotation: ", annot);
console.log("Action: ", event.action);
});
}
onExportAnnotationCommand = (event: {action: string, xfdfCommand: string, annotations: Array<AnnotOptions.Annotation>}) => {
event.annotations.forEach((annot) => {
console.log("Exported Annotation: ", annot);
console.log("Action: ", event.action);
})
}
onBehaviourActivated = (event: {action: Config.Actions, data: AnnotOptions.LinkPressData | AnnotOptions.StickyNoteData}) => {
console.log("Action is ", event.action);
console.log("Data is ", event.data);
}
onPageChanged = (event: {previousPageNumber: number, pageNumber: number}) => {
console.log('Previous page:', event.previousPageNumber, 'current page:', event.pageNumber);
}
onToolChanged = (event: {previousTool: Config.Tools | "unknown tool", tool: Config.Tools | "unknown tool"}) => {
console.log('Previous tool: ', event.previousTool, " current tool: ", event.tool);
}
// A callback for when button 1 is pressed
buttonFunction1 = async () => {
if (_viewer != null) {
let ret = await _viewer.gotoPreviousPage();
console.log("Return value: ", ret);
}
}
// A callback for when button 2 is pressed
buttonFunction2 = async () => {
if (_viewer != null) {
let ret = await _viewer.gotoNextPage();
console.log("Return value: ", ret);
}
}
render() {
// If the component's state indicates that storage permissions have not been granted,
// a view is loaded prompting users to grant these permissions.
if (!this.state.permissionGranted) {
return (
<View style={styles.container}>
<Text>
Storage permission required.
</Text>
</View>
)
}
const path = "https://pdftron.s3.amazonaws.com/downloads/pl/PDFTRON_mobile_about.pdf";
const toolbar = {
[Config.CustomToolbarKey.Id]: 'toolbar',
[Config.CustomToolbarKey.Name]: 'Custom Toolbar',
[Config.CustomToolbarKey.Icon] : Config.ToolbarIcons.Draw,
[Config.CustomToolbarKey.Items]: [
Config.Tools.annotationCreateFreeHand,
Config.Tools.annotationCreateArrow,
Config.Tools.annotationCreateFreeText,
Config.Tools.annotationCreateLine,
Config.Tools.annotationEraserTool,
Config.Buttons.undo,
Config.Buttons.redo
],
};
return (
<View style={styles.container}>
<DocumentView
ref={(ref) => _viewer = ref}
document={path}
showLeadingNavButton={true}
onLeadingNavButtonPressed={this.onLeadingNavButtonPressed}
onDocumentLoaded={this.onDocumentLoaded}
// overrideAnnotationMenuBehavior={[Config.AnnotationMenu.flatten]}
// onAnnotationMenuPress={this.onAnnotationMenuPress}
onAnnotationChanged={this.onAnnotationChanged}
onAnnotationsSelected={this.onAnnotationSelected}
onExportAnnotationCommand={this.onExportAnnotationCommand}
// overrideBehavior={[Config.Actions.linkPress, Config.Actions.stickyNoteShowPopUp]}
// onBehaviorActivated={this.onBehaviourActivated}
// onPageChanged={this.onPageChanged}
// onToolChanged={this.onToolChanged}
disabledElements={[Config.Buttons.saveCopyButton]}
annotationToolbars={[toolbar]}
/>
<View style={styles.buttonView}>
<Button title="1" onPress={this.buttonFunction1}/>
<Button title="2" onPress={this.buttonFunction2}/>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
buttonView : {
flexDirection: 'row',
height: 50,
justifyContent: 'space-between'
}
});
Please follow this guide to upgrade example app to the latest version of React Native