289 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| /**
 | |
|  * @licstart The following is the entire license notice for the
 | |
|  * JavaScript code in this page
 | |
|  *
 | |
|  * Copyright 2022 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.
 | |
|  *
 | |
|  * @licend The above is the entire license notice for the
 | |
|  * JavaScript code in this page
 | |
|  */
 | |
| "use strict";
 | |
| 
 | |
| Object.defineProperty(exports, "__esModule", {
 | |
|   value: true
 | |
| });
 | |
| exports.TextHighlighter = void 0;
 | |
| 
 | |
| class TextHighlighter {
 | |
|   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;
 | |
|   }
 | |
| 
 | |
|   setTextMapping(divs, texts) {
 | |
|     this.textDivs = divs;
 | |
|     this.textContentItemsStr = texts;
 | |
|   }
 | |
| 
 | |
|   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;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _convertMatches(matches, matchesLength) {
 | |
|     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++) {
 | |
|       let matchIdx = matches[m];
 | |
| 
 | |
|       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
 | |
|         }
 | |
|       };
 | |
|       matchIdx += matchesLength[m];
 | |
| 
 | |
|       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) {
 | |
|     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) {
 | |
|       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;
 | |
| 
 | |
|       if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
 | |
|         if (prevEnd !== null) {
 | |
|           appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
 | |
|         }
 | |
| 
 | |
|         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) {
 | |
|         findController.scrollMatchIntoView({
 | |
|           element: textDivs[begin.divIdx],
 | |
|           selectedLeft,
 | |
|           pageIndex: pageIdx,
 | |
|           matchIndex: selectedMatchIdx
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (prevEnd) {
 | |
|       appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _updateMatches() {
 | |
|     if (!this.enabled) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const {
 | |
|       findController,
 | |
|       matches,
 | |
|       pageIdx
 | |
|     } = this;
 | |
|     const {
 | |
|       textContentItemsStr,
 | |
|       textDivs
 | |
|     } = this;
 | |
|     let clearedUntilDivIdx = -1;
 | |
| 
 | |
|     for (let i = 0, ii = matches.length; i < ii; i++) {
 | |
|       const match = matches[i];
 | |
|       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) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const pageMatches = findController.pageMatches[pageIdx] || null;
 | |
|     const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null;
 | |
|     this.matches = this._convertMatches(pageMatches, pageMatchesLength);
 | |
| 
 | |
|     this._renderMatches(this.matches);
 | |
|   }
 | |
| 
 | |
| }
 | |
| 
 | |
| exports.TextHighlighter = TextHighlighter; |