UNPKG

7.39 kBJavaScriptView Raw
1/**
2 * @licstart The following is the entire license notice for the
3 * JavaScript code in this page
4 *
5 * Copyright 2022 Mozilla Foundation
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 * @licend The above is the entire license notice for the
20 * JavaScript code in this page
21 */
22"use strict";
23
24Object.defineProperty(exports, "__esModule", {
25 value: true
26});
27exports.TextHighlighter = void 0;
28
29class TextHighlighter {
30 constructor({
31 findController,
32 eventBus,
33 pageIndex
34 }) {
35 this.findController = findController;
36 this.matches = [];
37 this.eventBus = eventBus;
38 this.pageIdx = pageIndex;
39 this._onUpdateTextLayerMatches = null;
40 this.textDivs = null;
41 this.textContentItemsStr = null;
42 this.enabled = false;
43 }
44
45 setTextMapping(divs, texts) {
46 this.textDivs = divs;
47 this.textContentItemsStr = texts;
48 }
49
50 enable() {
51 if (!this.textDivs || !this.textContentItemsStr) {
52 throw new Error("Text divs and strings have not been set.");
53 }
54
55 if (this.enabled) {
56 throw new Error("TextHighlighter is already enabled.");
57 }
58
59 this.enabled = true;
60
61 if (!this._onUpdateTextLayerMatches) {
62 this._onUpdateTextLayerMatches = evt => {
63 if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
64 this._updateMatches();
65 }
66 };
67
68 this.eventBus._on("updatetextlayermatches", this._onUpdateTextLayerMatches);
69 }
70
71 this._updateMatches();
72 }
73
74 disable() {
75 if (!this.enabled) {
76 return;
77 }
78
79 this.enabled = false;
80
81 if (this._onUpdateTextLayerMatches) {
82 this.eventBus._off("updatetextlayermatches", this._onUpdateTextLayerMatches);
83
84 this._onUpdateTextLayerMatches = null;
85 }
86 }
87
88 _convertMatches(matches, matchesLength) {
89 if (!matches) {
90 return [];
91 }
92
93 const {
94 textContentItemsStr
95 } = this;
96 let i = 0,
97 iIndex = 0;
98 const end = textContentItemsStr.length - 1;
99 const result = [];
100
101 for (let m = 0, mm = matches.length; m < mm; m++) {
102 let matchIdx = matches[m];
103
104 while (i !== end && matchIdx >= iIndex + textContentItemsStr[i].length) {
105 iIndex += textContentItemsStr[i].length;
106 i++;
107 }
108
109 if (i === textContentItemsStr.length) {
110 console.error("Could not find a matching mapping");
111 }
112
113 const match = {
114 begin: {
115 divIdx: i,
116 offset: matchIdx - iIndex
117 }
118 };
119 matchIdx += matchesLength[m];
120
121 while (i !== end && matchIdx > iIndex + textContentItemsStr[i].length) {
122 iIndex += textContentItemsStr[i].length;
123 i++;
124 }
125
126 match.end = {
127 divIdx: i,
128 offset: matchIdx - iIndex
129 };
130 result.push(match);
131 }
132
133 return result;
134 }
135
136 _renderMatches(matches) {
137 if (matches.length === 0) {
138 return;
139 }
140
141 const {
142 findController,
143 pageIdx
144 } = this;
145 const {
146 textContentItemsStr,
147 textDivs
148 } = this;
149 const isSelectedPage = pageIdx === findController.selected.pageIdx;
150 const selectedMatchIdx = findController.selected.matchIdx;
151 const highlightAll = findController.state.highlightAll;
152 let prevEnd = null;
153 const infinity = {
154 divIdx: -1,
155 offset: undefined
156 };
157
158 function beginText(begin, className) {
159 const divIdx = begin.divIdx;
160 textDivs[divIdx].textContent = "";
161 return appendTextToDiv(divIdx, 0, begin.offset, className);
162 }
163
164 function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
165 let div = textDivs[divIdx];
166
167 if (div.nodeType === Node.TEXT_NODE) {
168 const span = document.createElement("span");
169 div.before(span);
170 span.append(div);
171 textDivs[divIdx] = span;
172 div = span;
173 }
174
175 const content = textContentItemsStr[divIdx].substring(fromOffset, toOffset);
176 const node = document.createTextNode(content);
177
178 if (className) {
179 const span = document.createElement("span");
180 span.className = `${className} appended`;
181 span.append(node);
182 div.append(span);
183 return className.includes("selected") ? span.offsetLeft : 0;
184 }
185
186 div.append(node);
187 return 0;
188 }
189
190 let i0 = selectedMatchIdx,
191 i1 = i0 + 1;
192
193 if (highlightAll) {
194 i0 = 0;
195 i1 = matches.length;
196 } else if (!isSelectedPage) {
197 return;
198 }
199
200 for (let i = i0; i < i1; i++) {
201 const match = matches[i];
202 const begin = match.begin;
203 const end = match.end;
204 const isSelected = isSelectedPage && i === selectedMatchIdx;
205 const highlightSuffix = isSelected ? " selected" : "";
206 let selectedLeft = 0;
207
208 if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
209 if (prevEnd !== null) {
210 appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
211 }
212
213 beginText(begin);
214 } else {
215 appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
216 }
217
218 if (begin.divIdx === end.divIdx) {
219 selectedLeft = appendTextToDiv(begin.divIdx, begin.offset, end.offset, "highlight" + highlightSuffix);
220 } else {
221 selectedLeft = appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, "highlight begin" + highlightSuffix);
222
223 for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
224 textDivs[n0].className = "highlight middle" + highlightSuffix;
225 }
226
227 beginText(end, "highlight end" + highlightSuffix);
228 }
229
230 prevEnd = end;
231
232 if (isSelected) {
233 findController.scrollMatchIntoView({
234 element: textDivs[begin.divIdx],
235 selectedLeft,
236 pageIndex: pageIdx,
237 matchIndex: selectedMatchIdx
238 });
239 }
240 }
241
242 if (prevEnd) {
243 appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
244 }
245 }
246
247 _updateMatches() {
248 if (!this.enabled) {
249 return;
250 }
251
252 const {
253 findController,
254 matches,
255 pageIdx
256 } = this;
257 const {
258 textContentItemsStr,
259 textDivs
260 } = this;
261 let clearedUntilDivIdx = -1;
262
263 for (let i = 0, ii = matches.length; i < ii; i++) {
264 const match = matches[i];
265 const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
266
267 for (let n = begin, end = match.end.divIdx; n <= end; n++) {
268 const div = textDivs[n];
269 div.textContent = textContentItemsStr[n];
270 div.className = "";
271 }
272
273 clearedUntilDivIdx = match.end.divIdx + 1;
274 }
275
276 if (!findController?.highlightMatches) {
277 return;
278 }
279
280 const pageMatches = findController.pageMatches[pageIdx] || null;
281 const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null;
282 this.matches = this._convertMatches(pageMatches, pageMatchesLength);
283
284 this._renderMatches(this.matches);
285 }
286
287}
288
289exports.TextHighlighter = TextHighlighter;
\No newline at end of file