huridocs/uwazi

View on GitHub
app/react/PDF/components/PDF.js

Summary

Maintainability
A
0 mins
Test Coverage
A
91%
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { SelectionRegion, HandleTextSelection } from '@huridocs/react-text-selection-handler';
import { advancedSort } from 'app/utils/advancedSort';
import { PDFPage } from 'app/PDF';
import { isClient } from '../../utils';
import PDFJS from '../PDFJS';

const cMapUrl = '/legacy_character_maps/';
const cMapPacked = true;

class PDF extends Component {
  static getDerivedStateFromProps(props, state) {
    if (state.filename !== null && state.filename !== props.filename) {
      return { pdf: { numPages: 0 }, filename: props.filename };
    }

    return null;
  }

  constructor(props) {
    super(props);
    this._isMounted = false;
    this.state = { pdf: { numPages: 0 }, filename: props.filename };
    this.pagesLoaded = {};
    this.loadDocument(props.file);
    this.currentPage = '1';
    this.pages = {};
    this.pdfReady = false;

    this.pageUnloaded = this.pageUnloaded.bind(this);
    this.pageLoading = this.pageLoading.bind(this);
    this.onPageVisible = this.onPageVisible.bind(this);
    this.onPageHidden = this.onPageHidden.bind(this);
  }

  componentDidMount() {
    this._isMounted = true;
    if (this.pdfContainer) {
      document.addEventListener('textlayerrendered', e => {
        this.pageLoaded(e.detail.pageNumber);
      });
      document.addEventListener('textlayerrendered', this.props.onPageLoaded, { once: true });
    }
  }

  shouldComponentUpdate(nextProps, nextState) {
    return (
      nextProps.file !== this.props.file ||
      nextProps.filename !== this.props.filename ||
      nextProps.style !== this.props.style ||
      nextState.pdf !== this.state.pdf
    );
  }

  componentDidUpdate(prevProps) {
    if (prevProps.filename !== null && this.props.filename !== prevProps.filename) {
      this.pagesLoaded = {};
      this.loadDocument(prevProps.file);
    }

    if (this.state.pdf.numPages && !this.pdfReady) {
      this.pdfReady = true;
      this.props.onPDFReady();
    }
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  onPageVisible(page, visibility) {
    this.pages[page] = visibility;

    const pageWithMostVisibility = Object.keys(this.pages).reduce((memo, key) => {
      if (!this.pages[key - 1] || this.pages[key] > this.pages[key - 1]) {
        return key;
      }
      return memo;
    }, 1);

    if (this.currentPage !== pageWithMostVisibility) {
      this.currentPage = pageWithMostVisibility;
      this.props.onPageChange(Number(pageWithMostVisibility));
    }
  }

  onPageHidden(page) {
    delete this.pages[page];
  }

  loadDocument(file) {
    if (isClient) {
      PDFJS.getDocument({
        url: file,
        cMapUrl,
        cMapPacked,
        isEvalSupported: false,
      }).promise.then(pdf => {
        if (this._isMounted) {
          this.setState({ pdf });
        }
      });
    }
  }

  pageUnloaded(numPage) {
    delete this.pagesLoaded[numPage];
    this.loaded();
  }

  pageLoading(numPage) {
    this.pagesLoaded[numPage] = false;
  }

  pageLoaded(numPage) {
    this.pagesLoaded[numPage] = true;
    const allPagesLoaded =
      Object.keys(this.pagesLoaded)
        .map(p => this.pagesLoaded[p])
        .filter(p => !p).length === 0;
    if (allPagesLoaded) {
      this.loaded();
    }
  }

  loaded() {
    const pages = Object.keys(this.pagesLoaded).map(n => parseInt(n, 10));

    const allConsecutives = advancedSort(pages, { treatAs: 'number' }).reduce((memo, number) => {
      if (memo === false) {
        return memo;
      }

      if (memo === null) {
        return number;
      }

      return number - memo > 1 ? false : number;
    }, null);

    if (allConsecutives) {
      this.props.onLoad({
        pages,
      });
    }
  }

  render() {
    return (
      <div
        ref={ref => {
          this.pdfContainer = ref;
        }}
        style={this.props.style}
      >
        <HandleTextSelection
          onSelect={this.props.onTextSelection}
          onDeselect={this.props.onTextDeselection}
        >
          {(() => {
            const pages = [];
            for (let page = 1; page <= this.state.pdf.numPages; page += 1) {
              pages.push(
                <div className="page-wrapper" key={page}>
                  <SelectionRegion regionId={page.toString()}>
                    <PDFPage
                      onUnload={this.pageUnloaded}
                      onLoading={this.pageLoading}
                      onVisible={this.onPageVisible}
                      onHidden={this.onPageHidden}
                      page={page}
                      pdf={this.state.pdf}
                      highlightReference={this.props.highlightReference}
                    />
                  </SelectionRegion>
                </div>
              );
            }
            return pages;
          })()}
        </HandleTextSelection>
      </div>
    );
  }
}

PDF.defaultProps = {
  onPageLoaded: () => {},
  filename: null,
  onPageChange: () => {},
  onPDFReady: () => {},
  style: {},
  onTextSelection: () => {},
  onTextDeselection: () => {},
  highlightReference: () => {},
};

PDF.propTypes = {
  onPageChange: PropTypes.func,
  onTextSelection: PropTypes.func,
  onTextDeselection: PropTypes.func,
  onPageLoaded: PropTypes.func,
  onPDFReady: PropTypes.func,
  file: PropTypes.string.isRequired,
  filename: PropTypes.string,
  onLoad: PropTypes.func.isRequired,
  style: PropTypes.object,
  highlightReference: PropTypes.func,
};

export default PDF;