UNPKG

10.6 kBJavaScriptView Raw
1/**
2 * @licstart The following is the entire license notice for the
3 * Javascript code in this page
4 *
5 * Copyright 2020 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.DefaultTextLayerFactory = exports.TextLayerBuilder = void 0;
28
29var _ui_utils = require("./ui_utils.js");
30
31var _pdf = require("../pdf");
32
33const EXPAND_DIVS_TIMEOUT = 300;
34
35class TextLayerBuilder {
36 constructor({
37 textLayerDiv,
38 eventBus,
39 pageIndex,
40 viewport,
41 findController = null,
42 enhanceTextSelection = false
43 }) {
44 this.textLayerDiv = textLayerDiv;
45 this.eventBus = eventBus || (0, _ui_utils.getGlobalEventBus)();
46 this.textContent = null;
47 this.textContentItemsStr = [];
48 this.textContentStream = null;
49 this.renderingDone = false;
50 this.pageIdx = pageIndex;
51 this.pageNumber = this.pageIdx + 1;
52 this.matches = [];
53 this.viewport = viewport;
54 this.textDivs = [];
55 this.findController = findController;
56 this.textLayerRenderTask = null;
57 this.enhanceTextSelection = enhanceTextSelection;
58 this._onUpdateTextLayerMatches = null;
59
60 this._bindMouse();
61 }
62
63 _finishRendering() {
64 this.renderingDone = true;
65
66 if (!this.enhanceTextSelection) {
67 const endOfContent = document.createElement("div");
68 endOfContent.className = "endOfContent";
69 this.textLayerDiv.appendChild(endOfContent);
70 }
71
72 this.eventBus.dispatch("textlayerrendered", {
73 source: this,
74 pageNumber: this.pageNumber,
75 numTextDivs: this.textDivs.length
76 });
77 }
78
79 render(timeout = 0) {
80 if (!(this.textContent || this.textContentStream) || this.renderingDone) {
81 return;
82 }
83
84 this.cancel();
85 this.textDivs = [];
86 const textLayerFrag = document.createDocumentFragment();
87 this.textLayerRenderTask = (0, _pdf.renderTextLayer)({
88 textContent: this.textContent,
89 textContentStream: this.textContentStream,
90 container: textLayerFrag,
91 viewport: this.viewport,
92 textDivs: this.textDivs,
93 textContentItemsStr: this.textContentItemsStr,
94 timeout,
95 enhanceTextSelection: this.enhanceTextSelection
96 });
97 this.textLayerRenderTask.promise.then(() => {
98 this.textLayerDiv.appendChild(textLayerFrag);
99
100 this._finishRendering();
101
102 this._updateMatches();
103 }, function (reason) {});
104
105 if (!this._onUpdateTextLayerMatches) {
106 this._onUpdateTextLayerMatches = evt => {
107 if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
108 this._updateMatches();
109 }
110 };
111
112 this.eventBus._on("updatetextlayermatches", this._onUpdateTextLayerMatches);
113 }
114 }
115
116 cancel() {
117 if (this.textLayerRenderTask) {
118 this.textLayerRenderTask.cancel();
119 this.textLayerRenderTask = null;
120 }
121
122 if (this._onUpdateTextLayerMatches) {
123 this.eventBus._off("updatetextlayermatches", this._onUpdateTextLayerMatches);
124
125 this._onUpdateTextLayerMatches = null;
126 }
127 }
128
129 setTextContentStream(readableStream) {
130 this.cancel();
131 this.textContentStream = readableStream;
132 }
133
134 setTextContent(textContent) {
135 this.cancel();
136 this.textContent = textContent;
137 }
138
139 _convertMatches(matches, matchesLength) {
140 if (!matches) {
141 return [];
142 }
143
144 const {
145 findController,
146 textContentItemsStr
147 } = this;
148 let i = 0,
149 iIndex = 0;
150 const end = textContentItemsStr.length - 1;
151 const queryLen = findController.state.query.length;
152 const result = [];
153
154 for (let m = 0, mm = matches.length; m < mm; m++) {
155 let matchIdx = matches[m];
156
157 while (i !== end && matchIdx >= iIndex + textContentItemsStr[i].length) {
158 iIndex += textContentItemsStr[i].length;
159 i++;
160 }
161
162 if (i === textContentItemsStr.length) {
163 console.error("Could not find a matching mapping");
164 }
165
166 const match = {
167 begin: {
168 divIdx: i,
169 offset: matchIdx - iIndex
170 }
171 };
172
173 if (matchesLength) {
174 matchIdx += matchesLength[m];
175 } else {
176 matchIdx += queryLen;
177 }
178
179 while (i !== end && matchIdx > iIndex + textContentItemsStr[i].length) {
180 iIndex += textContentItemsStr[i].length;
181 i++;
182 }
183
184 match.end = {
185 divIdx: i,
186 offset: matchIdx - iIndex
187 };
188 result.push(match);
189 }
190
191 return result;
192 }
193
194 _renderMatches(matches) {
195 if (matches.length === 0) {
196 return;
197 }
198
199 const {
200 findController,
201 pageIdx,
202 textContentItemsStr,
203 textDivs
204 } = this;
205 const isSelectedPage = pageIdx === findController.selected.pageIdx;
206 const selectedMatchIdx = findController.selected.matchIdx;
207 const highlightAll = findController.state.highlightAll;
208 let prevEnd = null;
209 const infinity = {
210 divIdx: -1,
211 offset: undefined
212 };
213
214 function beginText(begin, className) {
215 const divIdx = begin.divIdx;
216 textDivs[divIdx].textContent = "";
217 appendTextToDiv(divIdx, 0, begin.offset, className);
218 }
219
220 function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
221 const div = textDivs[divIdx];
222 const content = textContentItemsStr[divIdx].substring(fromOffset, toOffset);
223 const node = document.createTextNode(content);
224
225 if (className) {
226 const span = document.createElement("span");
227 span.className = className;
228 span.appendChild(node);
229 div.appendChild(span);
230 return;
231 }
232
233 div.appendChild(node);
234 }
235
236 let i0 = selectedMatchIdx,
237 i1 = i0 + 1;
238
239 if (highlightAll) {
240 i0 = 0;
241 i1 = matches.length;
242 } else if (!isSelectedPage) {
243 return;
244 }
245
246 for (let i = i0; i < i1; i++) {
247 const match = matches[i];
248 const begin = match.begin;
249 const end = match.end;
250 const isSelected = isSelectedPage && i === selectedMatchIdx;
251 const highlightSuffix = isSelected ? " selected" : "";
252
253 if (isSelected) {
254 findController.scrollMatchIntoView({
255 element: textDivs[begin.divIdx],
256 pageIndex: pageIdx,
257 matchIndex: selectedMatchIdx
258 });
259 }
260
261 if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
262 if (prevEnd !== null) {
263 appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
264 }
265
266 beginText(begin);
267 } else {
268 appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
269 }
270
271 if (begin.divIdx === end.divIdx) {
272 appendTextToDiv(begin.divIdx, begin.offset, end.offset, "highlight" + highlightSuffix);
273 } else {
274 appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, "highlight begin" + highlightSuffix);
275
276 for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
277 textDivs[n0].className = "highlight middle" + highlightSuffix;
278 }
279
280 beginText(end, "highlight end" + highlightSuffix);
281 }
282
283 prevEnd = end;
284 }
285
286 if (prevEnd) {
287 appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
288 }
289 }
290
291 _updateMatches() {
292 if (!this.renderingDone) {
293 return;
294 }
295
296 const {
297 findController,
298 matches,
299 pageIdx,
300 textContentItemsStr,
301 textDivs
302 } = this;
303 let clearedUntilDivIdx = -1;
304
305 for (let i = 0, ii = matches.length; i < ii; i++) {
306 const match = matches[i];
307 const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
308
309 for (let n = begin, end = match.end.divIdx; n <= end; n++) {
310 const div = textDivs[n];
311 div.textContent = textContentItemsStr[n];
312 div.className = "";
313 }
314
315 clearedUntilDivIdx = match.end.divIdx + 1;
316 }
317
318 if (!findController || !findController.highlightMatches) {
319 return;
320 }
321
322 const pageMatches = findController.pageMatches[pageIdx] || null;
323 const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null;
324 this.matches = this._convertMatches(pageMatches, pageMatchesLength);
325
326 this._renderMatches(this.matches);
327 }
328
329 _bindMouse() {
330 const div = this.textLayerDiv;
331 let expandDivsTimer = null;
332 div.addEventListener("mousedown", evt => {
333 if (this.enhanceTextSelection && this.textLayerRenderTask) {
334 this.textLayerRenderTask.expandTextDivs(true);
335
336 if (expandDivsTimer) {
337 clearTimeout(expandDivsTimer);
338 expandDivsTimer = null;
339 }
340
341 return;
342 }
343
344 const end = div.querySelector(".endOfContent");
345
346 if (!end) {
347 return;
348 }
349
350 let adjustTop = evt.target !== div;
351 adjustTop = adjustTop && window.getComputedStyle(end).getPropertyValue("-moz-user-select") !== "none";
352
353 if (adjustTop) {
354 const divBounds = div.getBoundingClientRect();
355 const r = Math.max(0, (evt.pageY - divBounds.top) / divBounds.height);
356 end.style.top = (r * 100).toFixed(2) + "%";
357 }
358
359 end.classList.add("active");
360 });
361 div.addEventListener("mouseup", () => {
362 if (this.enhanceTextSelection && this.textLayerRenderTask) {
363 expandDivsTimer = setTimeout(() => {
364 if (this.textLayerRenderTask) {
365 this.textLayerRenderTask.expandTextDivs(false);
366 }
367
368 expandDivsTimer = null;
369 }, EXPAND_DIVS_TIMEOUT);
370 return;
371 }
372
373 const end = div.querySelector(".endOfContent");
374
375 if (!end) {
376 return;
377 }
378
379 end.style.top = "";
380 end.classList.remove("active");
381 });
382 }
383
384}
385
386exports.TextLayerBuilder = TextLayerBuilder;
387
388class DefaultTextLayerFactory {
389 createTextLayerBuilder(textLayerDiv, pageIndex, viewport, enhanceTextSelection = false, eventBus) {
390 return new TextLayerBuilder({
391 textLayerDiv,
392 pageIndex,
393 viewport,
394 enhanceTextSelection,
395 eventBus
396 });
397 }
398
399}
400
401exports.DefaultTextLayerFactory = DefaultTextLayerFactory;
\No newline at end of file