|
@@ -1,300 +0,0 @@
|
|
|
-/* Copyright 2021 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.
|
|
|
- */
|
|
|
-
|
|
|
-/** @typedef {import("./event_utils").EventBus} EventBus */
|
|
|
-// eslint-disable-next-line max-len
|
|
|
-/** @typedef {import("./pdf_find_controller").PDFFindController} PDFFindController */
|
|
|
-
|
|
|
-/**
|
|
|
- * @typedef {Object} TextHighlighterOptions
|
|
|
- * @property {PDFFindController} findController
|
|
|
- * @property {EventBus} eventBus - The application event bus.
|
|
|
- * @property {number} pageIndex - The page index.
|
|
|
- */
|
|
|
-
|
|
|
-/**
|
|
|
- * TextHighlighter handles highlighting matches from the FindController in
|
|
|
- * either the text layer or XFA layer depending on the type of document.
|
|
|
- */
|
|
|
-class TextHighlighter {
|
|
|
- /**
|
|
|
- * @param {TextHighlighterOptions} options
|
|
|
- */
|
|
|
- constructor({ findController, eventBus, pageIndex }) {
|
|
|
- this.findController = findController;
|
|
|
- this.matches = [];
|
|
|
- this.eventBus = eventBus;
|
|
|
- this.pageIdx = pageIndex;
|
|
|
- this._onUpdateTextLayerMatches = null;
|
|
|
- this.textDivs = null;
|
|
|
- this.textContentItemsStr = null;
|
|
|
- this.enabled = false;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Store two arrays that will map DOM nodes to text they should contain.
|
|
|
- * The arrays should be of equal length and the array element at each index
|
|
|
- * should correspond to the other. e.g.
|
|
|
- * `items[0] = "<span>Item 0</span>" and texts[0] = "Item 0";
|
|
|
- *
|
|
|
- * @param {Array<Node>} divs
|
|
|
- * @param {Array<string>} texts
|
|
|
- */
|
|
|
- setTextMapping(divs, texts) {
|
|
|
- this.textDivs = divs;
|
|
|
- this.textContentItemsStr = texts;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Start listening for events to update the highlighter and check if there are
|
|
|
- * any current matches that need be highlighted.
|
|
|
- */
|
|
|
- enable() {
|
|
|
- if (!this.textDivs || !this.textContentItemsStr) {
|
|
|
- throw new Error("Text divs and strings have not been set.");
|
|
|
- }
|
|
|
- if (this.enabled) {
|
|
|
- throw new Error("TextHighlighter is already enabled.");
|
|
|
- }
|
|
|
- this.enabled = true;
|
|
|
- if (!this._onUpdateTextLayerMatches) {
|
|
|
- this._onUpdateTextLayerMatches = evt => {
|
|
|
- if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
|
|
|
- this._updateMatches();
|
|
|
- }
|
|
|
- };
|
|
|
- this.eventBus._on(
|
|
|
- "updatetextlayermatches",
|
|
|
- this._onUpdateTextLayerMatches
|
|
|
- );
|
|
|
- }
|
|
|
- this._updateMatches();
|
|
|
- }
|
|
|
-
|
|
|
- disable() {
|
|
|
- if (!this.enabled) {
|
|
|
- return;
|
|
|
- }
|
|
|
- this.enabled = false;
|
|
|
- if (this._onUpdateTextLayerMatches) {
|
|
|
- this.eventBus._off(
|
|
|
- "updatetextlayermatches",
|
|
|
- this._onUpdateTextLayerMatches
|
|
|
- );
|
|
|
- this._onUpdateTextLayerMatches = null;
|
|
|
- }
|
|
|
- this._updateMatches(/* reset = */ true);
|
|
|
- }
|
|
|
-
|
|
|
- _convertMatches(matches, matchesLength) {
|
|
|
- // Early exit if there is nothing to convert.
|
|
|
- if (!matches) {
|
|
|
- return [];
|
|
|
- }
|
|
|
- const { textContentItemsStr } = this;
|
|
|
-
|
|
|
- let i = 0,
|
|
|
- iIndex = 0;
|
|
|
- const end = textContentItemsStr.length - 1;
|
|
|
- const result = [];
|
|
|
-
|
|
|
- for (let m = 0, mm = matches.length; m < mm; m++) {
|
|
|
- // Calculate the start position.
|
|
|
- let matchIdx = matches[m];
|
|
|
-
|
|
|
- // Loop over the divIdxs.
|
|
|
- while (i !== end && matchIdx >= iIndex + textContentItemsStr[i].length) {
|
|
|
- iIndex += textContentItemsStr[i].length;
|
|
|
- i++;
|
|
|
- }
|
|
|
-
|
|
|
- if (i === textContentItemsStr.length) {
|
|
|
- console.error("Could not find a matching mapping");
|
|
|
- }
|
|
|
-
|
|
|
- const match = {
|
|
|
- begin: {
|
|
|
- divIdx: i,
|
|
|
- offset: matchIdx - iIndex,
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- // Calculate the end position.
|
|
|
- matchIdx += matchesLength[m];
|
|
|
-
|
|
|
- // Somewhat the same array as above, but use > instead of >= to get
|
|
|
- // the end position right.
|
|
|
- while (i !== end && matchIdx > iIndex + textContentItemsStr[i].length) {
|
|
|
- iIndex += textContentItemsStr[i].length;
|
|
|
- i++;
|
|
|
- }
|
|
|
-
|
|
|
- match.end = {
|
|
|
- divIdx: i,
|
|
|
- offset: matchIdx - iIndex,
|
|
|
- };
|
|
|
- result.push(match);
|
|
|
- }
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
- _renderMatches(matches) {
|
|
|
- // Early exit if there is nothing to render.
|
|
|
- if (matches.length === 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const { findController, pageIdx } = this;
|
|
|
- const { textContentItemsStr, textDivs } = this;
|
|
|
-
|
|
|
- const isSelectedPage = pageIdx === findController.selected.pageIdx;
|
|
|
- const selectedMatchIdx = findController.selected.matchIdx;
|
|
|
- const highlightAll = findController.state.highlightAll;
|
|
|
- let prevEnd = null;
|
|
|
- const infinity = {
|
|
|
- divIdx: -1,
|
|
|
- offset: undefined,
|
|
|
- };
|
|
|
-
|
|
|
- function beginText(begin, className) {
|
|
|
- const divIdx = begin.divIdx;
|
|
|
- textDivs[divIdx].textContent = "";
|
|
|
- return appendTextToDiv(divIdx, 0, begin.offset, className);
|
|
|
- }
|
|
|
-
|
|
|
- function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
|
|
|
- let div = textDivs[divIdx];
|
|
|
- if (div.nodeType === Node.TEXT_NODE) {
|
|
|
- const span = document.createElement("span");
|
|
|
- div.before(span);
|
|
|
- span.append(div);
|
|
|
- textDivs[divIdx] = span;
|
|
|
- div = span;
|
|
|
- }
|
|
|
- const content = textContentItemsStr[divIdx].substring(
|
|
|
- fromOffset,
|
|
|
- toOffset
|
|
|
- );
|
|
|
- const node = document.createTextNode(content);
|
|
|
- if (className) {
|
|
|
- const span = document.createElement("span");
|
|
|
- span.className = `${className} appended`;
|
|
|
- span.append(node);
|
|
|
- div.append(span);
|
|
|
- return className.includes("selected") ? span.offsetLeft : 0;
|
|
|
- }
|
|
|
- div.append(node);
|
|
|
- return 0;
|
|
|
- }
|
|
|
-
|
|
|
- let 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 (let i = i0; i < i1; i++) {
|
|
|
- const match = matches[i];
|
|
|
- const begin = match.begin;
|
|
|
- const end = match.end;
|
|
|
- const isSelected = isSelectedPage && i === selectedMatchIdx;
|
|
|
- const highlightSuffix = isSelected ? " selected" : "";
|
|
|
- let selectedLeft = 0;
|
|
|
-
|
|
|
- // 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) {
|
|
|
- selectedLeft = appendTextToDiv(
|
|
|
- begin.divIdx,
|
|
|
- begin.offset,
|
|
|
- end.offset,
|
|
|
- "highlight" + highlightSuffix
|
|
|
- );
|
|
|
- } else {
|
|
|
- selectedLeft = appendTextToDiv(
|
|
|
- begin.divIdx,
|
|
|
- begin.offset,
|
|
|
- infinity.offset,
|
|
|
- "highlight begin" + highlightSuffix
|
|
|
- );
|
|
|
- for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
|
|
|
- textDivs[n0].className = "highlight middle" + highlightSuffix;
|
|
|
- }
|
|
|
- beginText(end, "highlight end" + highlightSuffix);
|
|
|
- }
|
|
|
- prevEnd = end;
|
|
|
-
|
|
|
- if (isSelected) {
|
|
|
- // Attempt to scroll the selected match into view.
|
|
|
- findController.scrollMatchIntoView({
|
|
|
- element: textDivs[begin.divIdx],
|
|
|
- selectedLeft,
|
|
|
- pageIndex: pageIdx,
|
|
|
- matchIndex: selectedMatchIdx,
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (prevEnd) {
|
|
|
- appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- _updateMatches(reset = false) {
|
|
|
- if (!this.enabled && !reset) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const { findController, matches, pageIdx } = this;
|
|
|
- const { textContentItemsStr, textDivs } = this;
|
|
|
- let clearedUntilDivIdx = -1;
|
|
|
-
|
|
|
- // Clear all current matches.
|
|
|
- for (const match of matches) {
|
|
|
- const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
|
|
|
- for (let n = begin, end = match.end.divIdx; n <= end; n++) {
|
|
|
- const div = textDivs[n];
|
|
|
- div.textContent = textContentItemsStr[n];
|
|
|
- div.className = "";
|
|
|
- }
|
|
|
- clearedUntilDivIdx = match.end.divIdx + 1;
|
|
|
- }
|
|
|
-
|
|
|
- if (!findController?.highlightMatches || reset) {
|
|
|
- return;
|
|
|
- }
|
|
|
- // Convert the matches on the `findController` into the match format
|
|
|
- // used for the textLayer.
|
|
|
- const pageMatches = findController.pageMatches[pageIdx] || null;
|
|
|
- const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null;
|
|
|
-
|
|
|
- this.matches = this._convertMatches(pageMatches, pageMatchesLength);
|
|
|
- this._renderMatches(this.matches);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export { TextHighlighter };
|