UNPKG

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