Merge pull request #10099 from timvandermeij/find-controller

[api-major] Rework the find controller for unit testing
This commit is contained in:
Tim van der Meij 2018-09-30 18:36:13 +02:00 committed by GitHub
commit ec10cae5b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 241 additions and 127 deletions

View File

@ -38,24 +38,24 @@ var container = document.getElementById('viewerContainer');
// (Optionally) enable hyperlinks within PDF files.
var pdfLinkService = new pdfjsViewer.PDFLinkService();
// (Optionally) enable find controller.
var pdfFindController = new pdfjsViewer.PDFFindController({
linkService: pdfLinkService,
});
var pdfViewer = new pdfjsViewer.PDFViewer({
container: container,
linkService: pdfLinkService,
findController: pdfFindController,
});
pdfLinkService.setViewer(pdfViewer);
// (Optionally) enable find controller.
var pdfFindController = new pdfjsViewer.PDFFindController({
pdfViewer: pdfViewer,
});
pdfViewer.setFindController(pdfFindController);
container.addEventListener('pagesinit', function () {
// We can use pdfViewer now, e.g. let's change default scale.
pdfViewer.currentScaleValue = 'page-width';
if (SEARCH_FOR) { // We can try search for things
pdfFindController.executeCommand('find', {query: SEARCH_FOR});
pdfFindController.executeCommand('find', { query: SEARCH_FOR, });
}
});
@ -70,4 +70,5 @@ pdfjsLib.getDocument({
pdfViewer.setDocument(pdfDocument);
pdfLinkService.setDocument(pdfDocument, null);
pdfFindController.setDocument(pdfDocument);
});

View File

@ -38,24 +38,24 @@ var container = document.getElementById('viewerContainer');
// (Optionally) enable hyperlinks within PDF files.
var pdfLinkService = new pdfjsViewer.PDFLinkService();
// (Optionally) enable find controller.
var pdfFindController = new pdfjsViewer.PDFFindController({
linkService: pdfLinkService,
});
var pdfSinglePageViewer = new pdfjsViewer.PDFSinglePageViewer({
container: container,
linkService: pdfLinkService,
findController: pdfFindController,
});
pdfLinkService.setViewer(pdfSinglePageViewer);
// (Optionally) enable find controller.
var pdfFindController = new pdfjsViewer.PDFFindController({
pdfViewer: pdfSinglePageViewer,
});
pdfSinglePageViewer.setFindController(pdfFindController);
container.addEventListener('pagesinit', function () {
// We can use pdfSinglePageViewer now, e.g. let's change default scale.
pdfSinglePageViewer.currentScaleValue = 'page-width';
if (SEARCH_FOR) { // We can try search for things
pdfFindController.executeCommand('find', {query: SEARCH_FOR});
pdfFindController.executeCommand('find', { query: SEARCH_FOR, });
}
});
@ -70,4 +70,5 @@ pdfjsLib.getDocument({
pdfSinglePageViewer.setDocument(pdfDocument);
pdfLinkService.setDocument(pdfDocument, null);
pdfFindController.setDocument(pdfDocument);
});

View File

@ -31,7 +31,6 @@ var CMAP_URL = '../../node_modules/pdfjs-dist/cmaps/';
var CMAP_PACKED = true;
var DEFAULT_URL = '../../web/compressed.tracemonkey-pldi-09.pdf';
var SEARCH_FOR = ''; // try 'Mozilla';
var container = document.getElementById('viewerContainer');
@ -46,19 +45,9 @@ var pdfViewer = new pdfjsViewer.PDFViewer({
});
pdfLinkService.setViewer(pdfViewer);
// (Optionally) enable find controller.
var pdfFindController = new pdfjsViewer.PDFFindController({
pdfViewer: pdfViewer,
});
pdfViewer.setFindController(pdfFindController);
container.addEventListener('pagesinit', function () {
// We can use pdfViewer now, e.g. let's change default scale.
pdfViewer.currentScaleValue = 'page-width';
if (SEARCH_FOR) { // We can try search for things
pdfFindController.executeCommand('find', {query: SEARCH_FOR});
}
});
// Loading document.

View File

@ -25,8 +25,9 @@
"network_utils_spec.js",
"node_stream_spec.js",
"parser_spec.js",
"pdf_find_utils.js",
"pdf_history.js",
"pdf_find_controller_spec.js",
"pdf_find_utils_spec.js",
"pdf_history_spec.js",
"primitives_spec.js",
"stream_spec.js",
"type1_parser_spec.js",

View File

@ -67,6 +67,7 @@ function initializePDFJS(callback) {
'pdfjs-test/unit/network_spec',
'pdfjs-test/unit/network_utils_spec',
'pdfjs-test/unit/parser_spec',
'pdfjs-test/unit/pdf_find_controller_spec',
'pdfjs-test/unit/pdf_find_utils_spec',
'pdfjs-test/unit/pdf_history_spec',
'pdfjs-test/unit/primitives_spec',

View File

@ -0,0 +1,99 @@
/* Copyright 2018 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { buildGetDocumentParams } from './test_utils';
import { EventBus } from '../../web/ui_utils';
import { getDocument } from '../../src/display/api';
import { PDFFindController } from '../../web/pdf_find_controller';
import { SimpleLinkService } from '../../web/pdf_link_service';
class MockLinkService extends SimpleLinkService {
constructor() {
super();
this._page = 1;
this._pdfDocument = null;
}
setDocument(pdfDocument) {
this._pdfDocument = pdfDocument;
}
get pagesCount() {
return this._pdfDocument.numPages;
}
get page() {
return this._page;
}
set page(value) {
this._page = value;
}
}
describe('pdf_find_controller', function() {
let eventBus;
let pdfFindController;
beforeEach(function(done) {
const loadingTask = getDocument(buildGetDocumentParams('tracemonkey.pdf'));
loadingTask.promise.then(function(pdfDocument) {
const linkService = new MockLinkService();
linkService.setDocument(pdfDocument);
eventBus = new EventBus();
pdfFindController = new PDFFindController({
linkService,
eventBus,
});
pdfFindController.setDocument(pdfDocument);
eventBus.dispatch('pagesinit');
done();
});
});
afterEach(function() {
eventBus = null;
pdfFindController = null;
});
it('performs a basic search', function(done) {
pdfFindController.executeCommand('find', { query: 'Dynamic', });
const matchesPerPage = [11, 5, 0, 3, 0, 0, 0, 1, 1, 1, 0, 3, 4, 4];
const totalPages = matchesPerPage.length;
const totalMatches = matchesPerPage.reduce((a, b) => {
return a + b;
});
eventBus.on('updatefindmatchescount',
function onUpdateFindMatchesCount(evt) {
if (pdfFindController.pageMatches.length !== totalPages) {
return;
}
eventBus.off('updatefindmatchescount', onUpdateFindMatchesCount);
expect(evt.matchesCount.total).toBe(totalMatches);
for (let i = 0; i < totalPages; i++) {
expect(pdfFindController.pageMatches[i].length)
.toEqual(matchesPerPage[i]);
}
done();
});
});
});

View File

@ -305,6 +305,12 @@ let PDFViewerApplication = {
});
this.downloadManager = downloadManager;
const findController = new PDFFindController({
linkService: pdfLinkService,
eventBus,
});
this.findController = findController;
let container = appConfig.mainContainer;
let viewer = appConfig.viewerContainer;
this.pdfViewer = new PDFViewer({
@ -314,6 +320,7 @@ let PDFViewerApplication = {
renderingQueue: pdfRenderingQueue,
linkService: pdfLinkService,
downloadManager,
findController,
renderer: AppOptions.get('renderer'),
enableWebGL: AppOptions.get('enableWebGL'),
l10n: this.l10n,
@ -342,34 +349,8 @@ let PDFViewerApplication = {
});
pdfLinkService.setHistory(this.pdfHistory);
this.findController = new PDFFindController({
pdfViewer: this.pdfViewer,
eventBus,
});
this.findController.onUpdateResultsCount = (matchesCount) => {
if (this.supportsIntegratedFind) {
this.externalServices.updateFindMatchesCount(matchesCount);
} else {
this.findBar.updateResultsCount(matchesCount);
}
};
this.findController.onUpdateState = (state, previous, matchesCount) => {
if (this.supportsIntegratedFind) {
this.externalServices.updateFindControlState({
result: state,
findPrevious: previous,
matchesCount,
});
} else {
this.findBar.updateUIState(state, previous, matchesCount);
}
};
this.pdfViewer.setFindController(this.findController);
// TODO: improve `PDFFindBar` constructor parameter passing
let findBarConfig = Object.create(appConfig.findBar);
findBarConfig.findController = this.findController;
findBarConfig.eventBus = eventBus;
this.findBar = new PDFFindBar(findBarConfig, this.l10n);
@ -593,6 +574,7 @@ let PDFViewerApplication = {
if (this.pdfDocument) {
this.pdfDocument = null;
this.findController.setDocument(null);
this.pdfThumbnailViewer.setDocument(null);
this.pdfViewer.setDocument(null);
this.pdfLinkService.setDocument(null);
@ -609,7 +591,6 @@ let PDFViewerApplication = {
this.pdfOutlineViewer.reset();
this.pdfAttachmentViewer.reset();
this.findController.reset();
this.findBar.reset();
this.toolbar.reset();
this.secondaryToolbar.reset();
@ -917,6 +898,7 @@ let PDFViewerApplication = {
} else if (PDFJSDev.test('CHROME')) {
baseDocumentUrl = location.href.split('#')[0];
}
this.findController.setDocument(pdfDocument);
this.pdfLinkService.setDocument(pdfDocument, baseDocumentUrl);
this.pdfDocumentProperties.setDocument(pdfDocument, this.url);
@ -1343,6 +1325,8 @@ let PDFViewerApplication = {
eventBus.on('documentproperties', webViewerDocumentProperties);
eventBus.on('find', webViewerFind);
eventBus.on('findfromurlhash', webViewerFindFromUrlHash);
eventBus.on('updatefindmatchescount', webViewerUpdateFindMatchesCount);
eventBus.on('updatefindcontrolstate', webViewerUpdateFindControlState);
if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) {
eventBus.on('fileinputchange', webViewerFileInputChange);
}
@ -1414,6 +1398,8 @@ let PDFViewerApplication = {
eventBus.off('documentproperties', webViewerDocumentProperties);
eventBus.off('find', webViewerFind);
eventBus.off('findfromurlhash', webViewerFindFromUrlHash);
eventBus.off('updatefindmatchescount', webViewerUpdateFindMatchesCount);
eventBus.off('updatefindcontrolstate', webViewerUpdateFindControlState);
if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) {
eventBus.off('fileinputchange', webViewerFileInputChange);
}
@ -1976,6 +1962,26 @@ function webViewerFindFromUrlHash(evt) {
});
}
function webViewerUpdateFindMatchesCount({ matchesCount, }) {
if (PDFViewerApplication.supportsIntegratedFind) {
PDFViewerApplication.externalServices.updateFindMatchesCount(matchesCount);
} else {
PDFViewerApplication.findBar.updateResultsCount(matchesCount);
}
}
function webViewerUpdateFindControlState({ state, previous, matchesCount, }) {
if (PDFViewerApplication.supportsIntegratedFind) {
PDFViewerApplication.externalServices.updateFindControlState({
result: state,
findPrevious: previous,
matchesCount,
});
} else {
PDFViewerApplication.findBar.updateUIState(state, previous, matchesCount);
}
}
function webViewerScaleChanging(evt) {
PDFViewerApplication.toolbar.setPageScale(evt.presetValue, evt.scale);

View File

@ -49,6 +49,8 @@ const SpreadMode = {
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {DownloadManager} downloadManager - (optional) The download
* manager component.
* @property {PDFFindController} findController - (optional) The find
* controller component.
* @property {PDFRenderingQueue} renderingQueue - (optional) The rendering
* queue object.
* @property {boolean} removePageBorders - (optional) Removes the border shadow
@ -142,6 +144,7 @@ class BaseViewer {
this.eventBus = options.eventBus || getGlobalEventBus();
this.linkService = options.linkService || new SimpleLinkService();
this.downloadManager = options.downloadManager || null;
this.findController = options.findController || null;
this.removePageBorders = options.removePageBorders || false;
this.textLayerMode = Number.isInteger(options.textLayerMode) ?
options.textLayerMode : TextLayerMode.ENABLE;
@ -913,14 +916,6 @@ class BaseViewer {
return false;
}
getPageTextContent(pageIndex) {
return this.pdfDocument.getPage(pageIndex + 1).then(function(page) {
return page.getTextContent({
normalizeWhitespace: true,
});
});
}
/**
* @param {HTMLDivElement} textLayerDiv
* @param {number} pageIndex
@ -963,10 +958,6 @@ class BaseViewer {
});
}
setFindController(findController) {
this.findController = findController;
}
/**
* @returns {boolean} Whether all pages of the PDF document have identical
* widths and heights.

View File

@ -184,7 +184,7 @@ class PDFDocumentProperties {
* Note that the overlay will contain no information if this method
* is not called.
*
* @param {Object} pdfDocument - A reference to the PDF document.
* @param {PDFDocumentProxy} pdfDocument - A reference to the PDF document.
* @param {string} url - The URL of the document.
*/
setDocument(pdfDocument, url = null) {

View File

@ -38,15 +38,9 @@ class PDFFindBar {
this.findResultsCount = options.findResultsCount || null;
this.findPreviousButton = options.findPreviousButton || null;
this.findNextButton = options.findNextButton || null;
this.findController = options.findController || null;
this.eventBus = options.eventBus;
this.l10n = l10n;
if (this.findController === null) {
throw new Error('PDFFindBar cannot be used without a ' +
'PDFFindController instance.');
}
// Add event listeners to the DOM elements.
this.toggleButton.addEventListener('click', () => {
this.toggle();

View File

@ -40,18 +40,24 @@ const CHARACTERS_TO_NORMALIZE = {
'\u00BE': '3/4', // Vulgar fraction three quarters
};
/**
* @typedef {Object} PDFFindControllerOptions
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {EventBus} eventBus - The application event bus.
*/
/**
* Provides search functionality to find a given string in a PDF document.
*/
class PDFFindController {
constructor({ pdfViewer, eventBus = getGlobalEventBus(), }) {
this._pdfViewer = pdfViewer;
/**
* @param {PDFFindControllerOptions} options
*/
constructor({ linkService, eventBus = getGlobalEventBus(), }) {
this._linkService = linkService;
this._eventBus = eventBus;
this.onUpdateResultsCount = null;
this.onUpdateState = null;
this.reset();
this._reset();
eventBus.on('findbarclose', () => {
this._highlightMatches = false;
@ -87,8 +93,51 @@ class PDFFindController {
return this._state;
}
reset() {
/**
* Set a reference to the PDF document in order to search it.
* Note that searching is not possible if this method is not called.
*
* @param {PDFDocumentProxy} pdfDocument - The PDF document to search.
*/
setDocument(pdfDocument) {
if (this._pdfDocument) {
this._reset();
}
if (!pdfDocument) {
return;
}
this._pdfDocument = pdfDocument;
}
executeCommand(cmd, state) {
if (!this._pdfDocument) {
return;
}
if (this._state === null || cmd !== 'findagain') {
this._dirtyMatch = true;
}
this._state = state;
this._updateUIState(FindState.PENDING);
this._firstPagePromise.then(() => {
this._extractText();
clearTimeout(this._findTimeout);
if (cmd === 'find') {
// Trigger the find action with a small delay to avoid starting the
// search when the user is still typing (saving resources).
this._findTimeout =
setTimeout(this._nextMatch.bind(this), FIND_TIMEOUT);
} else {
this._nextMatch();
}
});
}
_reset() {
this._highlightMatches = false;
this._pdfDocument = null;
this._pageMatches = [];
this._pageMatchesLength = null;
this._state = null;
@ -118,28 +167,6 @@ class PDFFindController {
});
}
executeCommand(cmd, state) {
if (this._state === null || cmd !== 'findagain') {
this._dirtyMatch = true;
}
this._state = state;
this._updateUIState(FindState.PENDING);
this._firstPagePromise.then(() => {
this._extractText();
clearTimeout(this._findTimeout);
if (cmd === 'find') {
// Trigger the find action with a small delay to avoid starting the
// search when the user is still typing (saving resources).
this._findTimeout =
setTimeout(this._nextMatch.bind(this), FIND_TIMEOUT);
} else {
this._nextMatch();
}
});
}
_normalize(text) {
return text.replace(this._normalizationRegex, function(ch) {
return CHARACTERS_TO_NORMALIZE[ch];
@ -321,12 +348,16 @@ class PDFFindController {
}
let promise = Promise.resolve();
for (let i = 0, ii = this._pdfViewer.pagesCount; i < ii; i++) {
for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) {
const extractTextCapability = createPromiseCapability();
this._extractTextPromises[i] = extractTextCapability.promise;
promise = promise.then(() => {
return this._pdfViewer.getPageTextContent(i).then((textContent) => {
return this._pdfDocument.getPage(i + 1).then((pdfPage) => {
return pdfPage.getTextContent({
normalizeWhitespace: true,
});
}).then((textContent) => {
const textItems = textContent.items;
const strBuf = [];
@ -350,21 +381,21 @@ class PDFFindController {
_updatePage(index) {
if (this._selected.pageIdx === index) {
// If the page is selected, scroll the page into view, which triggers
// rendering the page, which adds the textLayer. Once the textLayer is
// build, it will scroll onto the selected match.
this._pdfViewer.currentPageNumber = index + 1;
// rendering the page, which adds the text layer. Once the text layer
// is built, it will scroll to the selected match.
this._linkService.page = index + 1;
}
const page = this._pdfViewer.getPageView(index);
if (page.textLayer) {
page.textLayer.updateMatches();
}
this._eventBus.dispatch('updatetextlayermatches', {
source: this,
pageIndex: index,
});
}
_nextMatch() {
const previous = this._state.findPrevious;
const currentPageIndex = this._pdfViewer.currentPageNumber - 1;
const numPages = this._pdfViewer.pagesCount;
const currentPageIndex = this._linkService.page - 1;
const numPages = this._linkService.pagesCount;
this._highlightMatches = true;
@ -476,7 +507,7 @@ class PDFFindController {
_advanceOffsetPage(previous) {
const offset = this._offset;
const numPages = this._extractTextPromises.length;
const numPages = this._linkService.pagesCount;
offset.pageIdx = (previous ? offset.pageIdx - 1 : offset.pageIdx + 1);
offset.matchIdx = null;
@ -495,8 +526,8 @@ class PDFFindController {
if (found) {
const previousPage = this._selected.pageIdx;
this.selected.pageIdx = this._offset.pageIdx;
this.selected.matchIdx = this._offset.matchIdx;
this._selected.pageIdx = this._offset.pageIdx;
this._selected.matchIdx = this._offset.matchIdx;
state = (wrapped ? FindState.WRAPPED : FindState.FOUND);
// Update the currently selected page to wipe out any selected matches.
@ -530,19 +561,19 @@ class PDFFindController {
}
_updateUIResultsCount() {
if (!this.onUpdateResultsCount) {
return;
}
const matchesCount = this._requestMatchesCount();
this.onUpdateResultsCount(matchesCount);
this._eventBus.dispatch('updatefindmatchescount', {
source: this,
matchesCount: this._requestMatchesCount(),
});
}
_updateUIState(state, previous) {
if (!this.onUpdateState) {
return;
}
const matchesCount = this._requestMatchesCount();
this.onUpdateState(state, previous, matchesCount);
this._eventBus.dispatch('updatefindcontrolstate', {
source: this,
state,
previous,
matchesCount: this._requestMatchesCount(),
});
}
}

View File

@ -358,7 +358,7 @@ class TextLayerBuilder {
}
};
_boundEvents.updateTextLayerMatches = (evt) => {
if (evt.pageIndex !== -1) {
if (evt.pageIndex !== this.pageIdx && evt.pageIndex !== -1) {
return;
}
this.updateMatches();