UNPKG

14.8 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.PDFHistory = void 0;
28exports.isDestArraysEqual = isDestArraysEqual;
29exports.isDestHashesEqual = isDestHashesEqual;
30
31var _ui_utils = require("./ui_utils.js");
32
33var _event_utils = require("./event_utils.js");
34
35const HASH_CHANGE_TIMEOUT = 1000;
36const POSITION_UPDATED_THRESHOLD = 50;
37const UPDATE_VIEWAREA_TIMEOUT = 1000;
38
39function getCurrentHash() {
40 return document.location.hash;
41}
42
43class PDFHistory {
44 constructor({
45 linkService,
46 eventBus
47 }) {
48 this.linkService = linkService;
49 this.eventBus = eventBus;
50 this._initialized = false;
51 this._fingerprint = "";
52 this.reset();
53 this._boundEvents = null;
54
55 this.eventBus._on("pagesinit", () => {
56 this._isPagesLoaded = false;
57
58 this.eventBus._on("pagesloaded", evt => {
59 this._isPagesLoaded = !!evt.pagesCount;
60 }, {
61 once: true
62 });
63 });
64 }
65
66 initialize({
67 fingerprint,
68 resetHistory = false,
69 updateUrl = false
70 }) {
71 if (!fingerprint || typeof fingerprint !== "string") {
72 console.error('PDFHistory.initialize: The "fingerprint" must be a non-empty string.');
73 return;
74 }
75
76 if (this._initialized) {
77 this.reset();
78 }
79
80 const reInitialized = this._fingerprint !== "" && this._fingerprint !== fingerprint;
81 this._fingerprint = fingerprint;
82 this._updateUrl = updateUrl === true;
83 this._initialized = true;
84
85 this._bindEvents();
86
87 const state = window.history.state;
88 this._popStateInProgress = false;
89 this._blockHashChange = 0;
90 this._currentHash = getCurrentHash();
91 this._numPositionUpdates = 0;
92 this._uid = this._maxUid = 0;
93 this._destination = null;
94 this._position = null;
95
96 if (!this._isValidState(state, true) || resetHistory) {
97 const {
98 hash,
99 page,
100 rotation
101 } = this._parseCurrentHash(true);
102
103 if (!hash || reInitialized || resetHistory) {
104 this._pushOrReplaceState(null, true);
105
106 return;
107 }
108
109 this._pushOrReplaceState({
110 hash,
111 page,
112 rotation
113 }, true);
114
115 return;
116 }
117
118 const destination = state.destination;
119
120 this._updateInternalState(destination, state.uid, true);
121
122 if (destination.rotation !== undefined) {
123 this._initialRotation = destination.rotation;
124 }
125
126 if (destination.dest) {
127 this._initialBookmark = JSON.stringify(destination.dest);
128 this._destination.page = null;
129 } else if (destination.hash) {
130 this._initialBookmark = destination.hash;
131 } else if (destination.page) {
132 this._initialBookmark = `page=${destination.page}`;
133 }
134 }
135
136 reset() {
137 if (this._initialized) {
138 this._pageHide();
139
140 this._initialized = false;
141
142 this._unbindEvents();
143 }
144
145 if (this._updateViewareaTimeout) {
146 clearTimeout(this._updateViewareaTimeout);
147 this._updateViewareaTimeout = null;
148 }
149
150 this._initialBookmark = null;
151 this._initialRotation = null;
152 }
153
154 push({
155 namedDest = null,
156 explicitDest,
157 pageNumber
158 }) {
159 if (!this._initialized) {
160 return;
161 }
162
163 if (namedDest && typeof namedDest !== "string") {
164 console.error("PDFHistory.push: " + `"${namedDest}" is not a valid namedDest parameter.`);
165 return;
166 } else if (!Array.isArray(explicitDest)) {
167 console.error("PDFHistory.push: " + `"${explicitDest}" is not a valid explicitDest parameter.`);
168 return;
169 } else if (!this._isValidPage(pageNumber)) {
170 if (pageNumber !== null || this._destination) {
171 console.error("PDFHistory.push: " + `"${pageNumber}" is not a valid pageNumber parameter.`);
172 return;
173 }
174 }
175
176 const hash = namedDest || JSON.stringify(explicitDest);
177
178 if (!hash) {
179 return;
180 }
181
182 let forceReplace = false;
183
184 if (this._destination && (isDestHashesEqual(this._destination.hash, hash) || isDestArraysEqual(this._destination.dest, explicitDest))) {
185 if (this._destination.page) {
186 return;
187 }
188
189 forceReplace = true;
190 }
191
192 if (this._popStateInProgress && !forceReplace) {
193 return;
194 }
195
196 this._pushOrReplaceState({
197 dest: explicitDest,
198 hash,
199 page: pageNumber,
200 rotation: this.linkService.rotation
201 }, forceReplace);
202
203 if (!this._popStateInProgress) {
204 this._popStateInProgress = true;
205 Promise.resolve().then(() => {
206 this._popStateInProgress = false;
207 });
208 }
209 }
210
211 pushPage(pageNumber) {
212 if (!this._initialized) {
213 return;
214 }
215
216 if (!this._isValidPage(pageNumber)) {
217 console.error(`PDFHistory.pushPage: "${pageNumber}" is not a valid page number.`);
218 return;
219 }
220
221 if (this._destination?.page === pageNumber) {
222 return;
223 }
224
225 if (this._popStateInProgress) {
226 return;
227 }
228
229 this._pushOrReplaceState({
230 dest: null,
231 hash: `page=${pageNumber}`,
232 page: pageNumber,
233 rotation: this.linkService.rotation
234 });
235
236 if (!this._popStateInProgress) {
237 this._popStateInProgress = true;
238 Promise.resolve().then(() => {
239 this._popStateInProgress = false;
240 });
241 }
242 }
243
244 pushCurrentPosition() {
245 if (!this._initialized || this._popStateInProgress) {
246 return;
247 }
248
249 this._tryPushCurrentPosition();
250 }
251
252 back() {
253 if (!this._initialized || this._popStateInProgress) {
254 return;
255 }
256
257 const state = window.history.state;
258
259 if (this._isValidState(state) && state.uid > 0) {
260 window.history.back();
261 }
262 }
263
264 forward() {
265 if (!this._initialized || this._popStateInProgress) {
266 return;
267 }
268
269 const state = window.history.state;
270
271 if (this._isValidState(state) && state.uid < this._maxUid) {
272 window.history.forward();
273 }
274 }
275
276 get popStateInProgress() {
277 return this._initialized && (this._popStateInProgress || this._blockHashChange > 0);
278 }
279
280 get initialBookmark() {
281 return this._initialized ? this._initialBookmark : null;
282 }
283
284 get initialRotation() {
285 return this._initialized ? this._initialRotation : null;
286 }
287
288 _pushOrReplaceState(destination, forceReplace = false) {
289 const shouldReplace = forceReplace || !this._destination;
290 const newState = {
291 fingerprint: this._fingerprint,
292 uid: shouldReplace ? this._uid : this._uid + 1,
293 destination
294 };
295
296 this._updateInternalState(destination, newState.uid);
297
298 let newUrl;
299
300 if (this._updateUrl && destination?.hash) {
301 const baseUrl = document.location.href.split("#")[0];
302
303 if (!baseUrl.startsWith("file://")) {
304 newUrl = `${baseUrl}#${destination.hash}`;
305 }
306 }
307
308 if (shouldReplace) {
309 window.history.replaceState(newState, "", newUrl);
310 } else {
311 window.history.pushState(newState, "", newUrl);
312 }
313 }
314
315 _tryPushCurrentPosition(temporary = false) {
316 if (!this._position) {
317 return;
318 }
319
320 let position = this._position;
321
322 if (temporary) {
323 position = Object.assign(Object.create(null), this._position);
324 position.temporary = true;
325 }
326
327 if (!this._destination) {
328 this._pushOrReplaceState(position);
329
330 return;
331 }
332
333 if (this._destination.temporary) {
334 this._pushOrReplaceState(position, true);
335
336 return;
337 }
338
339 if (this._destination.hash === position.hash) {
340 return;
341 }
342
343 if (!this._destination.page && (POSITION_UPDATED_THRESHOLD <= 0 || this._numPositionUpdates <= POSITION_UPDATED_THRESHOLD)) {
344 return;
345 }
346
347 let forceReplace = false;
348
349 if (this._destination.page >= position.first && this._destination.page <= position.page) {
350 if (this._destination.dest !== undefined || !this._destination.first) {
351 return;
352 }
353
354 forceReplace = true;
355 }
356
357 this._pushOrReplaceState(position, forceReplace);
358 }
359
360 _isValidPage(val) {
361 return Number.isInteger(val) && val > 0 && val <= this.linkService.pagesCount;
362 }
363
364 _isValidState(state, checkReload = false) {
365 if (!state) {
366 return false;
367 }
368
369 if (state.fingerprint !== this._fingerprint) {
370 if (checkReload) {
371 if (typeof state.fingerprint !== "string" || state.fingerprint.length !== this._fingerprint.length) {
372 return false;
373 }
374
375 const [perfEntry] = performance.getEntriesByType("navigation");
376
377 if (perfEntry?.type !== "reload") {
378 return false;
379 }
380 } else {
381 return false;
382 }
383 }
384
385 if (!Number.isInteger(state.uid) || state.uid < 0) {
386 return false;
387 }
388
389 if (state.destination === null || typeof state.destination !== "object") {
390 return false;
391 }
392
393 return true;
394 }
395
396 _updateInternalState(destination, uid, removeTemporary = false) {
397 if (this._updateViewareaTimeout) {
398 clearTimeout(this._updateViewareaTimeout);
399 this._updateViewareaTimeout = null;
400 }
401
402 if (removeTemporary && destination?.temporary) {
403 delete destination.temporary;
404 }
405
406 this._destination = destination;
407 this._uid = uid;
408 this._maxUid = Math.max(this._maxUid, uid);
409 this._numPositionUpdates = 0;
410 }
411
412 _parseCurrentHash(checkNameddest = false) {
413 const hash = unescape(getCurrentHash()).substring(1);
414 const params = (0, _ui_utils.parseQueryString)(hash);
415 const nameddest = params.get("nameddest") || "";
416 let page = params.get("page") | 0;
417
418 if (!this._isValidPage(page) || checkNameddest && nameddest.length > 0) {
419 page = null;
420 }
421
422 return {
423 hash,
424 page,
425 rotation: this.linkService.rotation
426 };
427 }
428
429 _updateViewarea({
430 location
431 }) {
432 if (this._updateViewareaTimeout) {
433 clearTimeout(this._updateViewareaTimeout);
434 this._updateViewareaTimeout = null;
435 }
436
437 this._position = {
438 hash: location.pdfOpenParams.substring(1),
439 page: this.linkService.page,
440 first: location.pageNumber,
441 rotation: location.rotation
442 };
443
444 if (this._popStateInProgress) {
445 return;
446 }
447
448 if (POSITION_UPDATED_THRESHOLD > 0 && this._isPagesLoaded && this._destination && !this._destination.page) {
449 this._numPositionUpdates++;
450 }
451
452 if (UPDATE_VIEWAREA_TIMEOUT > 0) {
453 this._updateViewareaTimeout = setTimeout(() => {
454 if (!this._popStateInProgress) {
455 this._tryPushCurrentPosition(true);
456 }
457
458 this._updateViewareaTimeout = null;
459 }, UPDATE_VIEWAREA_TIMEOUT);
460 }
461 }
462
463 _popState({
464 state
465 }) {
466 const newHash = getCurrentHash(),
467 hashChanged = this._currentHash !== newHash;
468 this._currentHash = newHash;
469
470 if (!state) {
471 this._uid++;
472
473 const {
474 hash,
475 page,
476 rotation
477 } = this._parseCurrentHash();
478
479 this._pushOrReplaceState({
480 hash,
481 page,
482 rotation
483 }, true);
484
485 return;
486 }
487
488 if (!this._isValidState(state)) {
489 return;
490 }
491
492 this._popStateInProgress = true;
493
494 if (hashChanged) {
495 this._blockHashChange++;
496 (0, _event_utils.waitOnEventOrTimeout)({
497 target: window,
498 name: "hashchange",
499 delay: HASH_CHANGE_TIMEOUT
500 }).then(() => {
501 this._blockHashChange--;
502 });
503 }
504
505 const destination = state.destination;
506
507 this._updateInternalState(destination, state.uid, true);
508
509 if ((0, _ui_utils.isValidRotation)(destination.rotation)) {
510 this.linkService.rotation = destination.rotation;
511 }
512
513 if (destination.dest) {
514 this.linkService.goToDestination(destination.dest);
515 } else if (destination.hash) {
516 this.linkService.setHash(destination.hash);
517 } else if (destination.page) {
518 this.linkService.page = destination.page;
519 }
520
521 Promise.resolve().then(() => {
522 this._popStateInProgress = false;
523 });
524 }
525
526 _pageHide() {
527 if (!this._destination || this._destination.temporary) {
528 this._tryPushCurrentPosition();
529 }
530 }
531
532 _bindEvents() {
533 if (this._boundEvents) {
534 return;
535 }
536
537 this._boundEvents = {
538 updateViewarea: this._updateViewarea.bind(this),
539 popState: this._popState.bind(this),
540 pageHide: this._pageHide.bind(this)
541 };
542
543 this.eventBus._on("updateviewarea", this._boundEvents.updateViewarea);
544
545 window.addEventListener("popstate", this._boundEvents.popState);
546 window.addEventListener("pagehide", this._boundEvents.pageHide);
547 }
548
549 _unbindEvents() {
550 if (!this._boundEvents) {
551 return;
552 }
553
554 this.eventBus._off("updateviewarea", this._boundEvents.updateViewarea);
555
556 window.removeEventListener("popstate", this._boundEvents.popState);
557 window.removeEventListener("pagehide", this._boundEvents.pageHide);
558 this._boundEvents = null;
559 }
560
561}
562
563exports.PDFHistory = PDFHistory;
564
565function isDestHashesEqual(destHash, pushHash) {
566 if (typeof destHash !== "string" || typeof pushHash !== "string") {
567 return false;
568 }
569
570 if (destHash === pushHash) {
571 return true;
572 }
573
574 const nameddest = (0, _ui_utils.parseQueryString)(destHash).get("nameddest");
575
576 if (nameddest === pushHash) {
577 return true;
578 }
579
580 return false;
581}
582
583function isDestArraysEqual(firstDest, secondDest) {
584 function isEntryEqual(first, second) {
585 if (typeof first !== typeof second) {
586 return false;
587 }
588
589 if (Array.isArray(first) || Array.isArray(second)) {
590 return false;
591 }
592
593 if (first !== null && typeof first === "object" && second !== null) {
594 if (Object.keys(first).length !== Object.keys(second).length) {
595 return false;
596 }
597
598 for (const key in first) {
599 if (!isEntryEqual(first[key], second[key])) {
600 return false;
601 }
602 }
603
604 return true;
605 }
606
607 return first === second || Number.isNaN(first) && Number.isNaN(second);
608 }
609
610 if (!(Array.isArray(firstDest) && Array.isArray(secondDest))) {
611 return false;
612 }
613
614 if (firstDest.length !== secondDest.length) {
615 return false;
616 }
617
618 for (let i = 0, ii = firstDest.length; i < ii; i++) {
619 if (!isEntryEqual(firstDest[i], secondDest[i])) {
620 return false;
621 }
622 }
623
624 return true;
625}
\No newline at end of file