Skip to content

Contribution Guide (Internal)

akwanpdf edited this page Oct 22, 2021 · 7 revisions

TL;DR

  1. Setup: The standard setup is to use an outer repo for testing and an inner repo for development.
  2. Coding: Use the TypeScript (not JavaScript) files.
  3. Testing: Run npm start in the outer repo's example folder and proceed with testing.
  4. Making a New Branch and Pull Request: Make your new branch from dev branch, and make your PR to dev branch.

Setup

  1. Clone the repo. This is your "outer repo" and is used for testing the app.
  2. In example folder, run npm install.
  3. In example/node_modules/, delete react-native-pdftron.
  4. In example/node_modules/, clone the repo a second time and rename it to react-native-pdftron. This is the "inner repo", used for development.
  5. In example/node_modules/react-native-pdftron, run npm install.
  6. Run npm install -g typescript to install the TypeScript compiler globally.

Coding

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:

Adding Config constants

  1. Open the src/Config/Config.ts file.
  2. Add your constant(s) to the Config object.
  3. If you created a whole new Config category (like Buttons or Tools), scroll down to the Config module and add export type NewCategory = ValueOf<typeof Config.NewCategory>;

Adding an AnnotOptions type or interface

AnnotOptions is used to store reusable object types. These types can be useful in DocumentView methods and event listeners.

  1. Open the src/AnnotOptions/AnnotOptions.ts file.
  2. Add a new type or interface to represent the object.

Adding a DocumentView prop (excluding event listeners)

  1. Open the src/DocumentView/DocumentView.tsx file.
  2. 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.

Adding a DocumentView event listener

  1. Open the src/DocumentView/DocumentView.tsx file.
  2. Add your event listener to the propTypes object and use the func<>() helper method.
    • To represent objects that may show up more than once, like annotations or rects, use AnnotOptions.
  3. Add an if/else case for your event listener to DocumentView.onChange method.

Adding a DocumentView method

  1. Open the src/DocumentView/DocumentView.tsx file.
  2. Add your method to the DocumentView class.
    • The return type should always look like Promise<void | T> where T is the expected return type upon success. If your method returns void, use Promise<void>.

Adding an RNPdftron method

  1. Open the index.ts file.
  2. Add the method to the Pdftron interface.

Editing PDFViewCtrl

  1. Open the src/PDFViewCtrl/PDFViewCtrl.tsx file.
  2. Follow the same steps used for elements in DocumentView.

Testing

  1. First, generate JavaScript files from the TypeScript ones:
    • In the outer repo's example folder, run npm start, npm run android, or npm run ios.
    • OR go to the inner repo and run tsc (or npx tsc if you have not installed tsc globally).
  2. 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.

Making a New Branch and Pull Request

  1. Create a branch from dev branch.
  2. Update the source code (in *.ts files).
  3. Commit and push the changes with descriptive messages.
  4. Create a pull request to dev branch.

* Changes made to dev will automatically be applied to master. Then, JS files will be automatically generated in master.

Other

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 dev and copy your changes to it.

Usage Example

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'
  }
});
Clone this wiki locally