@ -16,17 +16,18 @@
import { createPromiseCapability } from 'pdfjs-lib' ;
import { createPromiseCapability } from 'pdfjs-lib' ;
import { scrollIntoView } from './ui_utils' ;
import { scrollIntoView } from './ui_utils' ;
var FindStates = {
const FindState = {
FIND _F OUND : 0 ,
FOUND : 0 ,
FIND _NOT FOUND: 1 ,
NOT _ FOUND: 1 ,
FIND _ WRAPPED: 2 ,
WRAPPED : 2 ,
FIND _ PENDING: 3 ,
PENDING : 3 ,
} ;
} ;
var FIND _SCROLL _OFFSET _TOP = - 50 ;
const FIND _SCROLL _OFFSET _TOP = - 50 ;
var FIND _SCROLL _OFFSET _LEFT = - 400 ;
const FIND _SCROLL _OFFSET _LEFT = - 400 ;
const FIND _TIMEOUT = 250 ; // ms
var CHARACTERS _TO _NORMALIZE = {
const CHARACTERS _TO _NORMALIZE = {
'\u2018' : '\'' , // Left single quotation mark
'\u2018' : '\'' , // Left single quotation mark
'\u2019' : '\'' , // Right single quotation mark
'\u2019' : '\'' , // Right single quotation mark
'\u201A' : '\'' , // Single low-9 quotation mark
'\u201A' : '\'' , // Single low-9 quotation mark
@ -41,12 +42,11 @@ var CHARACTERS_TO_NORMALIZE = {
} ;
} ;
/ * *
/ * *
* Provides "search" or "find" functionality for the PDF .
* Provides search functionality to find a given string in a PDF document .
* This object actually performs the search for a given string .
* /
* /
var PDFFindController = ( function PDFFindControllerClosure ( ) {
class PDFFindController {
function PDFFindController ( options ) {
constructor ( { pdfViewer , } ) {
this . pdfViewer = options . pdfViewer || null ;
this . pdfViewer = pdfViewer ;
this . onUpdateResultsCount = null ;
this . onUpdateResultsCount = null ;
this . onUpdateState = null ;
this . onUpdateState = null ;
@ -54,12 +54,11 @@ var PDFFindController = (function PDFFindControllerClosure() {
this . reset ( ) ;
this . reset ( ) ;
// Compile the regular expression for text normalization once.
// Compile the regular expression for text normalization once.
var replace = Object . keys ( CHARACTERS _TO _NORMALIZE ) . join ( '' ) ;
let replace = Object . keys ( CHARACTERS _TO _NORMALIZE ) . join ( '' ) ;
this . normalizationRegex = new RegExp ( '[' + replace + ']' , 'g' ) ;
this . normalizationRegex = new RegExp ( '[' + replace + ']' , 'g' ) ;
}
}
PDFFindController . prototype = {
reset ( ) {
reset : function PDFFindController _reset ( ) {
this . startedTextExtraction = false ;
this . startedTextExtraction = false ;
this . extractTextPromises = [ ] ;
this . extractTextPromises = [ ] ;
this . pendingFindMatches = Object . create ( null ) ;
this . pendingFindMatches = Object . create ( null ) ;
@ -85,36 +84,35 @@ var PDFFindController = (function PDFFindControllerClosure() {
this . _firstPagePromise = new Promise ( ( resolve ) => {
this . _firstPagePromise = new Promise ( ( resolve ) => {
this . resolveFirstPage = resolve ;
this . resolveFirstPage = resolve ;
} ) ;
} ) ;
} ,
}
normalize : function PDFFindController _ normalize( text ) {
normalize ( text ) {
return text . replace ( this . normalizationRegex , function ( ch ) {
return text . replace ( this . normalizationRegex , function ( ch ) {
return CHARACTERS _TO _NORMALIZE [ ch ] ;
return CHARACTERS _TO _NORMALIZE [ ch ] ;
} ) ;
} ) ;
} ,
}
// Helper for multiple search - fills matchesWithLength array
// and takes into account cases when one search term
// include another search term (for example, "tamed tame" or "this is").
// Looking for intersecting terms in the 'matches' and
// leave elements with a longer match-length.
_prepareMatches : function PDFFindController _prepareMatches (
matchesWithLength , matches , matchesLength ) {
/ * *
* Helper for multi - term search that fills the ` matchesWithLength ` array
* and handles cases where one search term includes another search term ( for
* example , "tamed tame" or "this is" ) . It looks for intersecting terms in
* the ` matches ` and keeps elements with a longer match length .
* /
_prepareMatches ( matchesWithLength , matches , matchesLength ) {
function isSubTerm ( matchesWithLength , currentIndex ) {
function isSubTerm ( matchesWithLength , currentIndex ) {
var currentElem , prevElem , nextElem ;
let currentElem = matchesWithLength [ currentIndex ] ;
currentElem = matchesWithLength [ currentIndex ] ;
let nex tElem = matchesWithLength [ currentIndex + 1 ] ;
nextElem = matchesWithLength [ currentIndex + 1 ] ;
// checking for cases like "TAMEd TAME"
// Check for cases like "TAMEd TAME".
if ( currentIndex < matchesWithLength . length - 1 &&
if ( currentIndex < matchesWithLength . length - 1 &&
currentElem . match === nextElem . match ) {
currentElem . match === nextElem . match ) {
currentElem . skipped = true ;
currentElem . skipped = true ;
return true ;
return true ;
}
}
// checking for cases like "thIS IS"
for ( var i = currentIndex - 1 ; i >= 0 ; i -- ) {
// Check for cases like "thIS IS".
prevElem = matchesWithLength [ i ] ;
for ( let i = currentIndex - 1 ; i >= 0 ; i -- ) {
let prevElem = matchesWithLength [ i ] ;
if ( prevElem . skipped ) {
if ( prevElem . skipped ) {
continue ;
continue ;
}
}
@ -130,27 +128,25 @@ var PDFFindController = (function PDFFindControllerClosure() {
return false ;
return false ;
}
}
var i , len ;
// Sort the array of `{ match: <match>, matchLength: <matchLength> }`
// Sorting array of objects { match: <match>, matchLength: <matchLength> }
// objects on increasing index first and on the length otherwise.
// in increasing index first and then the lengths.
matchesWithLength . sort ( function ( a , b ) {
matchesWithLength . sort ( function ( a , b ) {
return a . match === b . match ?
return a . match === b . match ? a . matchLength - b . matchLength :
a . matchLength - b . matchLength : a . match - b . match ;
a . match - b . match ;
} ) ;
} ) ;
for ( i = 0 , len = matchesWithLength . length ; i < len ; i ++ ) {
for ( let i = 0 , len = matchesWithLength . length ; i < len ; i ++ ) {
if ( isSubTerm ( matchesWithLength , i ) ) {
if ( isSubTerm ( matchesWithLength , i ) ) {
continue ;
continue ;
}
}
matches . push ( matchesWithLength [ i ] . match ) ;
matches . push ( matchesWithLength [ i ] . match ) ;
matchesLength . push ( matchesWithLength [ i ] . matchLength ) ;
matchesLength . push ( matchesWithLength [ i ] . matchLength ) ;
}
}
} ,
}
calcFindPhraseMatch : function PDFFindController _calcFindPhraseMatch (
calcFindPhraseMatch ( query , pageIndex , pageContent ) {
query , pageIndex , pageContent ) {
let matches = [ ] ;
var matches = [ ] ;
let queryLen = query . length ;
var queryLen = query . length ;
let matchIdx = - queryLen ;
var matchIdx = - queryLen ;
while ( true ) {
while ( true ) {
matchIdx = pageContent . indexOf ( query , matchIdx + queryLen ) ;
matchIdx = pageContent . indexOf ( query , matchIdx + queryLen ) ;
if ( matchIdx === - 1 ) {
if ( matchIdx === - 1 ) {
@ -159,18 +155,16 @@ var PDFFindController = (function PDFFindControllerClosure() {
matches . push ( matchIdx ) ;
matches . push ( matchIdx ) ;
}
}
this . pageMatches [ pageIndex ] = matches ;
this . pageMatches [ pageIndex ] = matches ;
} ,
}
calcFindWordMatch : function PDFFindController _calcFindWordMatch (
calcFindWordMatch ( query , pageIndex , pageContent ) {
query , pageIndex , pageContent ) {
let matchesWithLength = [ ] ;
var matchesWithLength = [ ] ;
// Divide the query into pieces and search for text in each piece.
// Divide the query into pieces and search for text on each piece.
let queryArray = query . match ( /\S+/g ) ;
var queryArray = query . match ( /\S+/g ) ;
for ( let i = 0 , len = queryArray . length ; i < len ; i ++ ) {
var subquery , subqueryLen , matchIdx ;
let subquery = queryArray [ i ] ;
for ( var i = 0 , len = queryArray . length ; i < len ; i ++ ) {
let subqueryLen = subquery . length ;
subquery = queryArray [ i ] ;
let matchIdx = - subqueryLen ;
subqueryLen = subquery . length ;
matchIdx = - subqueryLen ;
while ( true ) {
while ( true ) {
matchIdx = pageContent . indexOf ( subquery , matchIdx + subqueryLen ) ;
matchIdx = pageContent . indexOf ( subquery , matchIdx + subqueryLen ) ;
if ( matchIdx === - 1 ) {
if ( matchIdx === - 1 ) {
@ -184,24 +178,26 @@ var PDFFindController = (function PDFFindControllerClosure() {
} ) ;
} ) ;
}
}
}
}
// Prepare arrays for store the matches.
// Prepare arrays for storing the matches.
if ( ! this . pageMatchesLength ) {
if ( ! this . pageMatchesLength ) {
this . pageMatchesLength = [ ] ;
this . pageMatchesLength = [ ] ;
}
}
this . pageMatchesLength [ pageIndex ] = [ ] ;
this . pageMatchesLength [ pageIndex ] = [ ] ;
this . pageMatches [ pageIndex ] = [ ] ;
this . pageMatches [ pageIndex ] = [ ] ;
// Sort matchesWithLength, clean up intersecting terms
// and put the result into the two arrays.
// Sort `matchesWithLength`, remove intersecting terms and put the result
// into the two arrays.
this . _prepareMatches ( matchesWithLength , this . pageMatches [ pageIndex ] ,
this . _prepareMatches ( matchesWithLength , this . pageMatches [ pageIndex ] ,
this . pageMatchesLength [ pageIndex ] ) ;
this . pageMatchesLength [ pageIndex ] ) ;
} ,
}
calcFindMatch : function PDFFindController _ calcFindMatch( pageIndex ) {
calcFindMatch ( pageIndex ) {
var pageContent = this . normalize ( this . pageContents [ pageIndex ] ) ;
let pageContent = this . normalize ( this . pageContents [ pageIndex ] ) ;
var query = this . normalize ( this . state . query ) ;
let query = this . normalize ( this . state . query ) ;
var caseSensitive = this . state . caseSensitive ;
let caseSensitive = this . state . caseSensitive ;
var phraseSearch = this . state . phraseSearch ;
let phraseSearch = this . state . phraseSearch ;
var queryLen = query . length ;
let queryLen = query . length ;
if ( queryLen === 0 ) {
if ( queryLen === 0 ) {
// Do nothing: the matches should be wiped out already.
// Do nothing: the matches should be wiped out already.
@ -225,12 +221,12 @@ var PDFFindController = (function PDFFindControllerClosure() {
this . nextPageMatch ( ) ;
this . nextPageMatch ( ) ;
}
}
// Update the matches count
// Update the match count.
if ( this . pageMatches [ pageIndex ] . length > 0 ) {
if ( this . pageMatches [ pageIndex ] . length > 0 ) {
this . matchCount += this . pageMatches [ pageIndex ] . length ;
this . matchCount += this . pageMatches [ pageIndex ] . length ;
this . updateUIResultsCount ( ) ;
this . updateUIResultsCount ( ) ;
}
}
} ,
}
extractText ( ) {
extractText ( ) {
if ( this . startedTextExtraction ) {
if ( this . startedTextExtraction ) {
@ -258,29 +254,30 @@ var PDFFindController = (function PDFFindControllerClosure() {
} ) ;
} ) ;
} ) ;
} ) ;
}
}
} ,
}
executeCommand : function PDFFindController _ executeCommand( cmd , state ) {
executeCommand ( cmd , state ) {
if ( this . state === null || cmd !== 'findagain' ) {
if ( this . state === null || cmd !== 'findagain' ) {
this . dirtyMatch = true ;
this . dirtyMatch = true ;
}
}
this . state = state ;
this . state = state ;
this . updateUIState ( FindStates . FIND _ PENDING) ;
this . updateUIState ( FindState . PENDING ) ;
this . _firstPagePromise . then ( ( ) => {
this . _firstPagePromise . then ( ( ) => {
this . extractText ( ) ;
this . extractText ( ) ;
clearTimeout ( this . findTimeout ) ;
clearTimeout ( this . findTimeout ) ;
if ( cmd === 'find' ) {
if ( cmd === 'find' ) {
// Only trigger the find action after 250ms of silence.
// Trigger the find action with a small delay to avoid starting the
this . findTimeout = setTimeout ( this . nextMatch . bind ( this ) , 250 ) ;
// search when the user is still typing (saving resources).
this . findTimeout = setTimeout ( this . nextMatch . bind ( this ) , FIND _TIMEOUT ) ;
} else {
} else {
this . nextMatch ( ) ;
this . nextMatch ( ) ;
}
}
} ) ;
} ) ;
} ,
}
updatePage : function PDFFindController _ updatePage( index ) {
updatePage ( index ) {
if ( this . selected . pageIdx === index ) {
if ( this . selected . pageIdx === index ) {
// If the page is selected, scroll the page into view, which triggers
// If the page is selected, scroll the page into view, which triggers
// rendering the page, which adds the textLayer. Once the textLayer is
// rendering the page, which adds the textLayer. Once the textLayer is
@ -288,16 +285,16 @@ var PDFFindController = (function PDFFindControllerClosure() {
this . pdfViewer . currentPageNumber = index + 1 ;
this . pdfViewer . currentPageNumber = index + 1 ;
}
}
var page = this . pdfViewer . getPageView ( index ) ;
let page = this . pdfViewer . getPageView ( index ) ;
if ( page . textLayer ) {
if ( page . textLayer ) {
page . textLayer . updateMatches ( ) ;
page . textLayer . updateMatches ( ) ;
}
}
} ,
}
nextMatch : function PDFFindController _ nextMatch( ) {
nextMatch ( ) {
var previous = this . state . findPrevious ;
let previous = this . state . findPrevious ;
var currentPageIndex = this . pdfViewer . currentPageNumber - 1 ;
let currentPageIndex = this . pdfViewer . currentPageNumber - 1 ;
var numPages = this . pdfViewer . pagesCount ;
let numPages = this . pdfViewer . pagesCount ;
this . active = true ;
this . active = true ;
@ -314,10 +311,10 @@ var PDFFindController = (function PDFFindControllerClosure() {
this . pageMatchesLength = null ;
this . pageMatchesLength = null ;
for ( let i = 0 ; i < numPages ; i ++ ) {
for ( let i = 0 ; i < numPages ; i ++ ) {
// Wipe out any previous highlighted matches.
// Wipe out any previously highlighted matches.
this . updatePage ( i ) ;
this . updatePage ( i ) ;
// As soon as the text is extracted start finding the matches .
// Start finding the matches as soon as the text is extracted .
if ( ! ( i in this . pendingFindMatches ) ) {
if ( ! ( i in this . pendingFindMatches ) ) {
this . pendingFindMatches [ i ] = true ;
this . pendingFindMatches [ i ] = true ;
this . extractTextPromises [ i ] . then ( ( pageIdx ) => {
this . extractTextPromises [ i ] . then ( ( pageIdx ) => {
@ -330,7 +327,7 @@ var PDFFindController = (function PDFFindControllerClosure() {
// If there's no query there's no point in searching.
// If there's no query there's no point in searching.
if ( this . state . query === '' ) {
if ( this . state . query === '' ) {
this . updateUIState ( FindStates . FIND _ FOUND) ;
this . updateUIState ( FindState . FOUND ) ;
return ;
return ;
}
}
@ -339,13 +336,13 @@ var PDFFindController = (function PDFFindControllerClosure() {
return ;
return ;
}
}
var offset = this . offset ;
let offset = this . offset ;
// Keep track of how many pages we should maximally iterate through.
// Keep track of how many pages we should maximally iterate through.
this . pagesToSearch = numPages ;
this . pagesToSearch = numPages ;
// If there's already a matchIdx that means we are iterating through a
// If there's already a ` matchIdx` that means we are iterating through a
// page's matches.
// page's matches.
if ( offset . matchIdx !== null ) {
if ( offset . matchIdx !== null ) {
var numPageMatches = this . pageMatches [ offset . pageIdx ] . length ;
let numPageMatches = this . pageMatches [ offset . pageIdx ] . length ;
if ( ( ! previous && offset . matchIdx + 1 < numPageMatches ) ||
if ( ( ! previous && offset . matchIdx + 1 < numPageMatches ) ||
( previous && offset . matchIdx > 0 ) ) {
( previous && offset . matchIdx > 0 ) ) {
// The simple case; we just have advance the matchIdx to select
// The simple case; we just have advance the matchIdx to select
@ -362,15 +359,15 @@ var PDFFindController = (function PDFFindControllerClosure() {
}
}
// Start searching through the page.
// Start searching through the page.
this . nextPageMatch ( ) ;
this . nextPageMatch ( ) ;
} ,
}
matchesReady : function PDFFindController _ matchesReady( matches ) {
matchesReady ( matches ) {
var offset = this . offset ;
let offset = this . offset ;
var numMatches = matches . length ;
let numMatches = matches . length ;
var previous = this . state . findPrevious ;
let previous = this . state . findPrevious ;
if ( numMatches ) {
if ( numMatches ) {
// There were matches for the page, so initialize the matchIdx .
// There were matches for the page, so initialize `matchIdx` .
this . hadMatch = true ;
this . hadMatch = true ;
offset . matchIdx = ( previous ? numMatches - 1 : 0 ) ;
offset . matchIdx = ( previous ? numMatches - 1 : 0 ) ;
this . updateMatch ( true ) ;
this . updateMatch ( true ) ;
@ -383,55 +380,56 @@ var PDFFindController = (function PDFFindControllerClosure() {
if ( this . pagesToSearch < 0 ) {
if ( this . pagesToSearch < 0 ) {
// No point in wrapping again, there were no matches.
// No point in wrapping again, there were no matches.
this . updateMatch ( false ) ;
this . updateMatch ( false ) ;
// w hile matches were not found, searching for a page
// W hile matches were not found, searching for a page
// with matches should nevertheless halt.
// with matches should nevertheless halt.
return true ;
return true ;
}
}
}
}
// Matches were not found (and searching is not done).
// Matches were not found (and searching is not done).
return false ;
return false ;
} ,
}
/ * *
/ * *
* The method is called back from the text layer when match presentation
* Called from the text layer when match presentation is updated .
* is updated .
*
* @ param { number } pageIndex - page index .
* @ param { number } pageIndex - The index of the page .
* @ param { number } index - match index .
* @ param { number } matchIndex - The index of the match .
* @ param { Array } elements - text layer div elements array .
* @ param { Array } elements - Text layer ` div ` elements .
* @ param { number } beginIdx - s tart index of the div array for the match .
* @ param { number } beginIdx - S tart index of the ` div ` array for the match .
* /
* /
updateMatchPosition : function PDFFindController _updateMatchPosition (
updateMatchPosition ( pageIndex , matchIndex , elements , beginIdx ) {
pageIndex , index , elements , beginIdx ) {
if ( this . selected . matchIdx === matchIndex &&
if ( this . selected . matchIdx === index &&
this . selected . pageIdx === pageIndex ) {
this . selected . pageIdx === pageIndex ) {
var spot = {
let spot = {
top : FIND _SCROLL _OFFSET _TOP ,
top : FIND _SCROLL _OFFSET _TOP ,
left : FIND _SCROLL _OFFSET _LEFT ,
left : FIND _SCROLL _OFFSET _LEFT ,
} ;
} ;
scrollIntoView ( elements [ beginIdx ] , spot ,
scrollIntoView ( elements [ beginIdx ] , spot ,
/* skipOverflowHiddenElements = */ true ) ;
/* skipOverflowHiddenElements = */ true ) ;
}
}
} ,
}
nextPageMatch : function PDFFindController _ nextPageMatch( ) {
nextPageMatch ( ) {
if ( this . resumePageIdx !== null ) {
if ( this . resumePageIdx !== null ) {
console . error ( 'There can only be one pending page.' ) ;
console . error ( 'There can only be one pending page.' ) ;
}
}
let matches = null ;
do {
do {
var pageIdx = this . offset . pageIdx ;
let pageIdx = this . offset . pageIdx ;
var matches = this . pageMatches [ pageIdx ] ;
matches = this . pageMatches [ pageIdx ] ;
if ( ! matches ) {
if ( ! matches ) {
// The matches don't exist yet for processing by "matchesReady" ,
// The matches don't exist yet for processing by `matchesReady` ,
// so set a resume point for when they do exist.
// so set a resume point for when they do exist.
this . resumePageIdx = pageIdx ;
this . resumePageIdx = pageIdx ;
break ;
break ;
}
}
} while ( ! this . matchesReady ( matches ) ) ;
} while ( ! this . matchesReady ( matches ) ) ;
} ,
}
advanceOffsetPage : function PDFFindController _ advanceOffsetPage( previous ) {
advanceOffsetPage ( previous ) {
var offset = this . offset ;
let offset = this . offset ;
var numPages = this . extractTextPromises . length ;
let numPages = this . extractTextPromises . length ;
offset . pageIdx = ( previous ? offset . pageIdx - 1 : offset . pageIdx + 1 ) ;
offset . pageIdx = ( previous ? offset . pageIdx - 1 : offset . pageIdx + 1 ) ;
offset . matchIdx = null ;
offset . matchIdx = null ;
@ -441,18 +439,19 @@ var PDFFindController = (function PDFFindControllerClosure() {
offset . pageIdx = ( previous ? numPages - 1 : 0 ) ;
offset . pageIdx = ( previous ? numPages - 1 : 0 ) ;
offset . wrapped = true ;
offset . wrapped = true ;
}
}
} ,
}
updateMatch : function PDFFindController _updateMatch ( found ) {
updateMatch ( found = false ) {
var state = FindStates . FIND _NOT FOUND ;
let state = FindState . NOT _ FOUND;
var wrapped = this . offset . wrapped ;
let wrapped = this . offset . wrapped ;
this . offset . wrapped = false ;
this . offset . wrapped = false ;
if ( found ) {
if ( found ) {
var previousPage = this . selected . pageIdx ;
let previousPage = this . selected . pageIdx ;
this . selected . pageIdx = this . offset . pageIdx ;
this . selected . pageIdx = this . offset . pageIdx ;
this . selected . matchIdx = this . offset . matchIdx ;
this . selected . matchIdx = this . offset . matchIdx ;
state = ( wrapped ? FindStates . FIND _WRAPPED : FindStates . FIND _FOUND ) ;
state = ( wrapped ? FindState . WRAPPED : FindState . FOUND ) ;
// Update the currently selected page to wipe out any selected matches.
// Update the currently selected page to wipe out any selected matches.
if ( previousPage !== - 1 && previousPage !== this . selected . pageIdx ) {
if ( previousPage !== - 1 && previousPage !== this . selected . pageIdx ) {
this . updatePage ( previousPage ) ;
this . updatePage ( previousPage ) ;
@ -463,25 +462,22 @@ var PDFFindController = (function PDFFindControllerClosure() {
if ( this . selected . pageIdx !== - 1 ) {
if ( this . selected . pageIdx !== - 1 ) {
this . updatePage ( this . selected . pageIdx ) ;
this . updatePage ( this . selected . pageIdx ) ;
}
}
} ,
}
updateUIResultsCount :
updateUIResultsCount ( ) {
function PDFFindController _updateUIResultsCount ( ) {
if ( this . onUpdateResultsCount ) {
if ( this . onUpdateResultsCount ) {
this . onUpdateResultsCount ( this . matchCount ) ;
this . onUpdateResultsCount ( this . matchCount ) ;
}
}
} ,
}
updateUIState : function PDFFindController _ updateUIState( state , previous ) {
updateUIState ( state , previous ) {
if ( this . onUpdateState ) {
if ( this . onUpdateState ) {
this . onUpdateState ( state , previous , this . matchCount ) ;
this . onUpdateState ( state , previous , this . matchCount ) ;
}
}
} ,
}
} ;
}
return PDFFindController ;
} ) ( ) ;
export {
export {
FindStates ,
FindState ,
PDFFindController ,
PDFFindController ,
} ;
} ;