6a7012aaca
These have been found using `gulp lint` in combination with the `unused: true` parameter for JSHint. Unfortunately there are too many false positives to enable this feature, but now that most globals have been removed because of the conversion to UMD the results are much more useful than before.
366 lines
12 KiB
JavaScript
366 lines
12 KiB
JavaScript
/* Copyright 2012 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.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
(function (root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
define('pdfjs-web/text_layer_builder', ['exports', 'pdfjs-web/dom_events',
|
|
'pdfjs-web/pdfjs'],
|
|
factory);
|
|
} else if (typeof exports !== 'undefined') {
|
|
factory(exports, require('./dom_events.js'), require('./pdfjs.js'));
|
|
} else {
|
|
factory((root.pdfjsWebTextLayerBuilder = {}), root.pdfjsWebDOMEvents,
|
|
root.pdfjsWebPDFJS);
|
|
}
|
|
}(this, function (exports, domEvents, pdfjsLib) {
|
|
|
|
/**
|
|
* @typedef {Object} TextLayerBuilderOptions
|
|
* @property {HTMLDivElement} textLayerDiv - The text layer container.
|
|
* @property {EventBus} eventBus - The application event bus.
|
|
* @property {number} pageIndex - The page index.
|
|
* @property {PageViewport} viewport - The viewport of the text layer.
|
|
* @property {PDFFindController} findController
|
|
*/
|
|
|
|
/**
|
|
* TextLayerBuilder provides text-selection functionality for the PDF.
|
|
* It does this by creating overlay divs over the PDF text. These divs
|
|
* contain text that matches the PDF text they are overlaying. This object
|
|
* also provides a way to highlight text that is being searched for.
|
|
* @class
|
|
*/
|
|
var TextLayerBuilder = (function TextLayerBuilderClosure() {
|
|
function TextLayerBuilder(options) {
|
|
this.textLayerDiv = options.textLayerDiv;
|
|
this.eventBus = options.eventBus || domEvents.getGlobalEventBus();
|
|
this.renderingDone = false;
|
|
this.divContentDone = false;
|
|
this.pageIdx = options.pageIndex;
|
|
this.pageNumber = this.pageIdx + 1;
|
|
this.matches = [];
|
|
this.viewport = options.viewport;
|
|
this.textDivs = [];
|
|
this.findController = options.findController || null;
|
|
this.textLayerRenderTask = null;
|
|
this._bindMouse();
|
|
}
|
|
|
|
TextLayerBuilder.prototype = {
|
|
_finishRendering: function TextLayerBuilder_finishRendering() {
|
|
this.renderingDone = true;
|
|
|
|
var endOfContent = document.createElement('div');
|
|
endOfContent.className = 'endOfContent';
|
|
this.textLayerDiv.appendChild(endOfContent);
|
|
|
|
this.eventBus.dispatch('textlayerrendered', {
|
|
source: this,
|
|
pageNumber: this.pageNumber
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Renders the text layer.
|
|
* @param {number} timeout (optional) if specified, the rendering waits
|
|
* for specified amount of ms.
|
|
*/
|
|
render: function TextLayerBuilder_render(timeout) {
|
|
if (!this.divContentDone || this.renderingDone) {
|
|
return;
|
|
}
|
|
|
|
if (this.textLayerRenderTask) {
|
|
this.textLayerRenderTask.cancel();
|
|
this.textLayerRenderTask = null;
|
|
}
|
|
|
|
this.textDivs = [];
|
|
var textLayerFrag = document.createDocumentFragment();
|
|
this.textLayerRenderTask = pdfjsLib.renderTextLayer({
|
|
textContent: this.textContent,
|
|
container: textLayerFrag,
|
|
viewport: this.viewport,
|
|
textDivs: this.textDivs,
|
|
timeout: timeout
|
|
});
|
|
this.textLayerRenderTask.promise.then(function () {
|
|
this.textLayerDiv.appendChild(textLayerFrag);
|
|
this._finishRendering();
|
|
this.updateMatches();
|
|
}.bind(this), function (reason) {
|
|
// canceled or failed to render text layer -- skipping errors
|
|
});
|
|
},
|
|
|
|
setTextContent: function TextLayerBuilder_setTextContent(textContent) {
|
|
if (this.textLayerRenderTask) {
|
|
this.textLayerRenderTask.cancel();
|
|
this.textLayerRenderTask = null;
|
|
}
|
|
this.textContent = textContent;
|
|
this.divContentDone = true;
|
|
},
|
|
|
|
convertMatches: function TextLayerBuilder_convertMatches(matches) {
|
|
var i = 0;
|
|
var iIndex = 0;
|
|
var bidiTexts = this.textContent.items;
|
|
var end = bidiTexts.length - 1;
|
|
var queryLen = (this.findController === null ?
|
|
0 : this.findController.state.query.length);
|
|
var ret = [];
|
|
|
|
for (var m = 0, len = matches.length; m < len; m++) {
|
|
// Calculate the start position.
|
|
var matchIdx = matches[m];
|
|
|
|
// Loop over the divIdxs.
|
|
while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
|
|
iIndex += bidiTexts[i].str.length;
|
|
i++;
|
|
}
|
|
|
|
if (i === bidiTexts.length) {
|
|
console.error('Could not find a matching mapping');
|
|
}
|
|
|
|
var match = {
|
|
begin: {
|
|
divIdx: i,
|
|
offset: matchIdx - iIndex
|
|
}
|
|
};
|
|
|
|
// Calculate the end position.
|
|
matchIdx += queryLen;
|
|
|
|
// Somewhat the same array as above, but use > instead of >= to get
|
|
// the end position right.
|
|
while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) {
|
|
iIndex += bidiTexts[i].str.length;
|
|
i++;
|
|
}
|
|
|
|
match.end = {
|
|
divIdx: i,
|
|
offset: matchIdx - iIndex
|
|
};
|
|
ret.push(match);
|
|
}
|
|
|
|
return ret;
|
|
},
|
|
|
|
renderMatches: function TextLayerBuilder_renderMatches(matches) {
|
|
// Early exit if there is nothing to render.
|
|
if (matches.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var bidiTexts = this.textContent.items;
|
|
var textDivs = this.textDivs;
|
|
var prevEnd = null;
|
|
var pageIdx = this.pageIdx;
|
|
var isSelectedPage = (this.findController === null ?
|
|
false : (pageIdx === this.findController.selected.pageIdx));
|
|
var selectedMatchIdx = (this.findController === null ?
|
|
-1 : this.findController.selected.matchIdx);
|
|
var highlightAll = (this.findController === null ?
|
|
false : this.findController.state.highlightAll);
|
|
var infinity = {
|
|
divIdx: -1,
|
|
offset: undefined
|
|
};
|
|
|
|
function beginText(begin, className) {
|
|
var divIdx = begin.divIdx;
|
|
textDivs[divIdx].textContent = '';
|
|
appendTextToDiv(divIdx, 0, begin.offset, className);
|
|
}
|
|
|
|
function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
|
|
var div = textDivs[divIdx];
|
|
var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset);
|
|
var node = document.createTextNode(content);
|
|
if (className) {
|
|
var span = document.createElement('span');
|
|
span.className = className;
|
|
span.appendChild(node);
|
|
div.appendChild(span);
|
|
return;
|
|
}
|
|
div.appendChild(node);
|
|
}
|
|
|
|
var i0 = selectedMatchIdx, i1 = i0 + 1;
|
|
if (highlightAll) {
|
|
i0 = 0;
|
|
i1 = matches.length;
|
|
} else if (!isSelectedPage) {
|
|
// Not highlighting all and this isn't the selected page, so do nothing.
|
|
return;
|
|
}
|
|
|
|
for (var i = i0; i < i1; i++) {
|
|
var match = matches[i];
|
|
var begin = match.begin;
|
|
var end = match.end;
|
|
var isSelected = (isSelectedPage && i === selectedMatchIdx);
|
|
var highlightSuffix = (isSelected ? ' selected' : '');
|
|
|
|
if (this.findController) {
|
|
this.findController.updateMatchPosition(pageIdx, i, textDivs,
|
|
begin.divIdx);
|
|
}
|
|
|
|
// Match inside new div.
|
|
if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
|
|
// If there was a previous div, then add the text at the end.
|
|
if (prevEnd !== null) {
|
|
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
|
|
}
|
|
// Clear the divs and set the content until the starting point.
|
|
beginText(begin);
|
|
} else {
|
|
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
|
|
}
|
|
|
|
if (begin.divIdx === end.divIdx) {
|
|
appendTextToDiv(begin.divIdx, begin.offset, end.offset,
|
|
'highlight' + highlightSuffix);
|
|
} else {
|
|
appendTextToDiv(begin.divIdx, begin.offset, infinity.offset,
|
|
'highlight begin' + highlightSuffix);
|
|
for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
|
|
textDivs[n0].className = 'highlight middle' + highlightSuffix;
|
|
}
|
|
beginText(end, 'highlight end' + highlightSuffix);
|
|
}
|
|
prevEnd = end;
|
|
}
|
|
|
|
if (prevEnd) {
|
|
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
|
|
}
|
|
},
|
|
|
|
updateMatches: function TextLayerBuilder_updateMatches() {
|
|
// Only show matches when all rendering is done.
|
|
if (!this.renderingDone) {
|
|
return;
|
|
}
|
|
|
|
// Clear all matches.
|
|
var matches = this.matches;
|
|
var textDivs = this.textDivs;
|
|
var bidiTexts = this.textContent.items;
|
|
var clearedUntilDivIdx = -1;
|
|
|
|
// Clear all current matches.
|
|
for (var i = 0, len = matches.length; i < len; i++) {
|
|
var match = matches[i];
|
|
var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
|
|
for (var n = begin, end = match.end.divIdx; n <= end; n++) {
|
|
var div = textDivs[n];
|
|
div.textContent = bidiTexts[n].str;
|
|
div.className = '';
|
|
}
|
|
clearedUntilDivIdx = match.end.divIdx + 1;
|
|
}
|
|
|
|
if (this.findController === null || !this.findController.active) {
|
|
return;
|
|
}
|
|
|
|
// Convert the matches on the page controller into the match format
|
|
// used for the textLayer.
|
|
this.matches = this.convertMatches(this.findController === null ?
|
|
[] : (this.findController.pageMatches[this.pageIdx] || []));
|
|
this.renderMatches(this.matches);
|
|
},
|
|
|
|
/**
|
|
* Fixes text selection: adds additional div where mouse was clicked.
|
|
* This reduces flickering of the content if mouse slowly dragged down/up.
|
|
* @private
|
|
*/
|
|
_bindMouse: function TextLayerBuilder_bindMouse() {
|
|
var div = this.textLayerDiv;
|
|
div.addEventListener('mousedown', function (e) {
|
|
var end = div.querySelector('.endOfContent');
|
|
if (!end) {
|
|
return;
|
|
}
|
|
//#if !(MOZCENTRAL || FIREFOX)
|
|
// On non-Firefox browsers, the selection will feel better if the height
|
|
// of the endOfContent div will be adjusted to start at mouse click
|
|
// location -- this will avoid flickering when selections moves up.
|
|
// However it does not work when selection started on empty space.
|
|
var adjustTop = e.target !== div;
|
|
//#if GENERIC
|
|
adjustTop = adjustTop && window.getComputedStyle(end).
|
|
getPropertyValue('-moz-user-select') !== 'none';
|
|
//#endif
|
|
if (adjustTop) {
|
|
var divBounds = div.getBoundingClientRect();
|
|
var r = Math.max(0, (e.pageY - divBounds.top) / divBounds.height);
|
|
end.style.top = (r * 100).toFixed(2) + '%';
|
|
}
|
|
//#endif
|
|
end.classList.add('active');
|
|
});
|
|
div.addEventListener('mouseup', function (e) {
|
|
var end = div.querySelector('.endOfContent');
|
|
if (!end) {
|
|
return;
|
|
}
|
|
//#if !(MOZCENTRAL || FIREFOX)
|
|
end.style.top = '';
|
|
//#endif
|
|
end.classList.remove('active');
|
|
});
|
|
},
|
|
};
|
|
return TextLayerBuilder;
|
|
})();
|
|
|
|
/**
|
|
* @constructor
|
|
* @implements IPDFTextLayerFactory
|
|
*/
|
|
function DefaultTextLayerFactory() {}
|
|
DefaultTextLayerFactory.prototype = {
|
|
/**
|
|
* @param {HTMLDivElement} textLayerDiv
|
|
* @param {number} pageIndex
|
|
* @param {PageViewport} viewport
|
|
* @returns {TextLayerBuilder}
|
|
*/
|
|
createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {
|
|
return new TextLayerBuilder({
|
|
textLayerDiv: textLayerDiv,
|
|
pageIndex: pageIndex,
|
|
viewport: viewport
|
|
});
|
|
}
|
|
};
|
|
|
|
exports.TextLayerBuilder = TextLayerBuilder;
|
|
exports.DefaultTextLayerFactory = DefaultTextLayerFactory;
|
|
}));
|