Merge pull request #8775 from Snuffleupagus/rewrite-PDFHistory-2
Re-write `PDFHistory` from scratch
This commit is contained in:
		
						commit
						1c9af00bee
					
				| @ -17,6 +17,7 @@ | |||||||
|     "murmurhash3_spec.js", |     "murmurhash3_spec.js", | ||||||
|     "node_stream_spec.js", |     "node_stream_spec.js", | ||||||
|     "parser_spec.js", |     "parser_spec.js", | ||||||
|  |     "pdf_history.js", | ||||||
|     "primitives_spec.js", |     "primitives_spec.js", | ||||||
|     "stream_spec.js", |     "stream_spec.js", | ||||||
|     "type1_parser_spec.js", |     "type1_parser_spec.js", | ||||||
|  | |||||||
| @ -64,6 +64,7 @@ function initializePDFJS(callback) { | |||||||
|     'pdfjs-test/unit/murmurhash3_spec', |     'pdfjs-test/unit/murmurhash3_spec', | ||||||
|     'pdfjs-test/unit/network_spec', |     'pdfjs-test/unit/network_spec', | ||||||
|     'pdfjs-test/unit/parser_spec', |     'pdfjs-test/unit/parser_spec', | ||||||
|  |     'pdfjs-test/unit/pdf_history_spec', | ||||||
|     'pdfjs-test/unit/primitives_spec', |     'pdfjs-test/unit/primitives_spec', | ||||||
|     'pdfjs-test/unit/stream_spec', |     'pdfjs-test/unit/stream_spec', | ||||||
|     'pdfjs-test/unit/type1_parser_spec', |     'pdfjs-test/unit/type1_parser_spec', | ||||||
|  | |||||||
							
								
								
									
										45
									
								
								test/unit/pdf_history_spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								test/unit/pdf_history_spec.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | |||||||
|  | /* Copyright 2017 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. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import { isDestsEqual } from '../../web/pdf_history'; | ||||||
|  | 
 | ||||||
|  | describe('pdf_history', function() { | ||||||
|  |   describe('isDestsEqual', function() { | ||||||
|  |     let firstDest = [{ num: 1, gen: 0, }, { name: 'XYZ', }, 0, 375, null]; | ||||||
|  |     let secondDest = [{ num: 5, gen: 0, }, { name: 'XYZ', }, 0, 375, null]; | ||||||
|  |     let thirdDest = [{ num: 1, gen: 0, }, { name: 'XYZ', }, 750, 0, null]; | ||||||
|  |     let fourthDest = [{ num: 1, gen: 0, }, { name: 'XYZ', }, 0, 375, 1.0]; | ||||||
|  |     let fifthDest = [{ gen: 0, num: 1, }, { name: 'XYZ', }, 0, 375, null]; | ||||||
|  | 
 | ||||||
|  |     it('should reject non-equal destination arrays', function() { | ||||||
|  |       expect(isDestsEqual(firstDest, undefined)).toEqual(false); | ||||||
|  |       expect(isDestsEqual(firstDest, [1, 2, 3, 4, 5])).toEqual(false); | ||||||
|  | 
 | ||||||
|  |       expect(isDestsEqual(firstDest, secondDest)).toEqual(false); | ||||||
|  |       expect(isDestsEqual(firstDest, thirdDest)).toEqual(false); | ||||||
|  |       expect(isDestsEqual(firstDest, fourthDest)).toEqual(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should accept equal destination arrays', function() { | ||||||
|  |       expect(isDestsEqual(firstDest, firstDest)).toEqual(true); | ||||||
|  |       expect(isDestsEqual(firstDest, fifthDest)).toEqual(true); | ||||||
|  | 
 | ||||||
|  |       let firstDestCopy = firstDest.slice(); | ||||||
|  |       expect(firstDest).not.toBe(firstDestCopy); | ||||||
|  | 
 | ||||||
|  |       expect(isDestsEqual(firstDest, firstDestCopy)).toEqual(true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -14,7 +14,8 @@ | |||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|   binarySearchFirstItem, EventBus, getPDFFileNameFromURL |   binarySearchFirstItem, EventBus, getPDFFileNameFromURL, waitOnEventOrTimeout, | ||||||
|  |   WaitOnType | ||||||
| } from '../../web/ui_utils'; | } from '../../web/ui_utils'; | ||||||
| import { createObjectURL, isNodeJS } from '../../src/shared/util'; | import { createObjectURL, isNodeJS } from '../../src/shared/util'; | ||||||
| 
 | 
 | ||||||
| @ -259,4 +260,118 @@ describe('ui_utils', function() { | |||||||
|       expect(count).toEqual(2); |       expect(count).toEqual(2); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   describe('waitOnEventOrTimeout', function() { | ||||||
|  |     let eventBus; | ||||||
|  | 
 | ||||||
|  |     beforeAll(function(done) { | ||||||
|  |       eventBus = new EventBus(); | ||||||
|  |       done(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     afterAll(function() { | ||||||
|  |       eventBus = null; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should reject invalid parameters', function(done) { | ||||||
|  |       let invalidTarget = waitOnEventOrTimeout({ | ||||||
|  |         target: 'window', | ||||||
|  |         name: 'DOMContentLoaded', | ||||||
|  |       }).then(function() { | ||||||
|  |         throw new Error('Should reject invalid parameters.'); | ||||||
|  |       }, function(reason) { | ||||||
|  |         expect(reason instanceof Error).toEqual(true); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       let invalidName = waitOnEventOrTimeout({ | ||||||
|  |         target: eventBus, | ||||||
|  |         name: '', | ||||||
|  |       }).then(function() { | ||||||
|  |         throw new Error('Should reject invalid parameters.'); | ||||||
|  |       }, function(reason) { | ||||||
|  |         expect(reason instanceof Error).toEqual(true); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       let invalidDelay = waitOnEventOrTimeout({ | ||||||
|  |         target: eventBus, | ||||||
|  |         name: 'pagerendered', | ||||||
|  |         delay: -1000, | ||||||
|  |       }).then(function() { | ||||||
|  |         throw new Error('Should reject invalid parameters.'); | ||||||
|  |       }, function(reason) { | ||||||
|  |         expect(reason instanceof Error).toEqual(true); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       Promise.all([invalidTarget, invalidName, invalidDelay]).then(done, | ||||||
|  |                                                                    done.fail); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should resolve on event, using the DOM', function(done) { | ||||||
|  |       if (isNodeJS()) { | ||||||
|  |         pending('Document in not supported in Node.js.'); | ||||||
|  |       } | ||||||
|  |       let button = document.createElement('button'); | ||||||
|  | 
 | ||||||
|  |       let buttonClicked = waitOnEventOrTimeout({ | ||||||
|  |         target: button, | ||||||
|  |         name: 'click', | ||||||
|  |         delay: 10000, | ||||||
|  |       }); | ||||||
|  |       // Immediately dispatch the expected event.
 | ||||||
|  |       button.click(); | ||||||
|  | 
 | ||||||
|  |       buttonClicked.then(function(type) { | ||||||
|  |         expect(type).toEqual(WaitOnType.EVENT); | ||||||
|  |         done(); | ||||||
|  |       }, done.fail); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should resolve on timeout, using the DOM', function(done) { | ||||||
|  |       if (isNodeJS()) { | ||||||
|  |         pending('Document in not supported in Node.js.'); | ||||||
|  |       } | ||||||
|  |       let button = document.createElement('button'); | ||||||
|  | 
 | ||||||
|  |       let buttonClicked = waitOnEventOrTimeout({ | ||||||
|  |         target: button, | ||||||
|  |         name: 'click', | ||||||
|  |         delay: 10, | ||||||
|  |       }); | ||||||
|  |       // Do *not* dispatch the event, and wait for the timeout.
 | ||||||
|  | 
 | ||||||
|  |       buttonClicked.then(function(type) { | ||||||
|  |         expect(type).toEqual(WaitOnType.TIMEOUT); | ||||||
|  |         done(); | ||||||
|  |       }, done.fail); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should resolve on event, using the EventBus', function(done) { | ||||||
|  |       let pageRendered = waitOnEventOrTimeout({ | ||||||
|  |         target: eventBus, | ||||||
|  |         name: 'pagerendered', | ||||||
|  |         delay: 10000, | ||||||
|  |       }); | ||||||
|  |       // Immediately dispatch the expected event.
 | ||||||
|  |       eventBus.dispatch('pagerendered'); | ||||||
|  | 
 | ||||||
|  |       pageRendered.then(function(type) { | ||||||
|  |         expect(type).toEqual(WaitOnType.EVENT); | ||||||
|  |         done(); | ||||||
|  |       }, done.fail); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should resolve on timeout, using the EventBus', function(done) { | ||||||
|  |       let pageRendered = waitOnEventOrTimeout({ | ||||||
|  |         target: eventBus, | ||||||
|  |         name: 'pagerendered', | ||||||
|  |         delay: 10, | ||||||
|  |       }); | ||||||
|  |       // Do *not* dispatch the event, and wait for the timeout.
 | ||||||
|  | 
 | ||||||
|  |       pageRendered.then(function(type) { | ||||||
|  |         expect(type).toEqual(WaitOnType.TIMEOUT); | ||||||
|  |         done(); | ||||||
|  |       }, done.fail); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
							
								
								
									
										53
									
								
								web/app.js
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								web/app.js
									
									
									
									
									
								
							| @ -89,7 +89,6 @@ const DefaultExternalServices = { | |||||||
| 
 | 
 | ||||||
| let PDFViewerApplication = { | let PDFViewerApplication = { | ||||||
|   initialBookmark: document.location.hash.substring(1), |   initialBookmark: document.location.hash.substring(1), | ||||||
|   initialDestination: null, |  | ||||||
|   initialized: false, |   initialized: false, | ||||||
|   fellback: false, |   fellback: false, | ||||||
|   appConfig: null, |   appConfig: null, | ||||||
| @ -931,21 +930,16 @@ let PDFViewerApplication = { | |||||||
|       if (!PDFJS.disableHistory && !this.isViewerEmbedded) { |       if (!PDFJS.disableHistory && !this.isViewerEmbedded) { | ||||||
|         // The browsing history is only enabled when the viewer is standalone,
 |         // The browsing history is only enabled when the viewer is standalone,
 | ||||||
|         // i.e. not when it is embedded in a web page.
 |         // i.e. not when it is embedded in a web page.
 | ||||||
|         if (!this.viewerPrefs['showPreviousViewOnLoad']) { |         let resetHistory = !this.viewerPrefs['showPreviousViewOnLoad']; | ||||||
|           this.pdfHistory.clearHistoryState(); |         this.pdfHistory.initialize(id, resetHistory); | ||||||
|         } |  | ||||||
|         this.pdfHistory.initialize(this.documentFingerprint); |  | ||||||
| 
 | 
 | ||||||
|         if (this.pdfHistory.initialDestination) { |         if (this.pdfHistory.initialBookmark) { | ||||||
|           this.initialDestination = this.pdfHistory.initialDestination; |  | ||||||
|         } else if (this.pdfHistory.initialBookmark) { |  | ||||||
|           this.initialBookmark = this.pdfHistory.initialBookmark; |           this.initialBookmark = this.pdfHistory.initialBookmark; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       let initialParams = { |       let initialParams = { | ||||||
|         destination: this.initialDestination, |         bookmark: null, | ||||||
|         bookmark: this.initialBookmark, |  | ||||||
|         hash: null, |         hash: null, | ||||||
|       }; |       }; | ||||||
|       let storePromise = store.getMultiple({ |       let storePromise = store.getMultiple({ | ||||||
| @ -979,9 +973,11 @@ let PDFViewerApplication = { | |||||||
|           sidebarView, |           sidebarView, | ||||||
|         }; |         }; | ||||||
|       }).then(({ hash, sidebarView, }) => { |       }).then(({ hash, sidebarView, }) => { | ||||||
|         this.setInitialView(hash, { sidebarView, }); |         initialParams.bookmark = this.initialBookmark; | ||||||
|         initialParams.hash = hash; |         initialParams.hash = hash; | ||||||
| 
 | 
 | ||||||
|  |         this.setInitialView(hash, { sidebarView, }); | ||||||
|  | 
 | ||||||
|         // Make all navigation keys work on document load,
 |         // Make all navigation keys work on document load,
 | ||||||
|         // unless the viewer is embedded in a web page.
 |         // unless the viewer is embedded in a web page.
 | ||||||
|         if (!this.isViewerEmbedded) { |         if (!this.isViewerEmbedded) { | ||||||
| @ -991,14 +987,12 @@ let PDFViewerApplication = { | |||||||
|       }).then(() => { |       }).then(() => { | ||||||
|         // For documents with different page sizes, once all pages are resolved,
 |         // For documents with different page sizes, once all pages are resolved,
 | ||||||
|         // ensure that the correct location becomes visible on load.
 |         // ensure that the correct location becomes visible on load.
 | ||||||
|         if (!initialParams.destination && !initialParams.bookmark && |         if (!initialParams.bookmark && !initialParams.hash) { | ||||||
|             !initialParams.hash) { |  | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|         if (pdfViewer.hasEqualPageSizes) { |         if (pdfViewer.hasEqualPageSizes) { | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|         this.initialDestination = initialParams.destination; |  | ||||||
|         this.initialBookmark = initialParams.bookmark; |         this.initialBookmark = initialParams.bookmark; | ||||||
| 
 | 
 | ||||||
|         pdfViewer.currentScaleValue = pdfViewer.currentScaleValue; |         pdfViewer.currentScaleValue = pdfViewer.currentScaleValue; | ||||||
| @ -1141,12 +1135,8 @@ let PDFViewerApplication = { | |||||||
|     this.isInitialViewSet = true; |     this.isInitialViewSet = true; | ||||||
|     this.pdfSidebar.setInitialView(sidebarView); |     this.pdfSidebar.setInitialView(sidebarView); | ||||||
| 
 | 
 | ||||||
|     if (this.initialDestination) { |     if (this.initialBookmark) { | ||||||
|       this.pdfLinkService.navigateTo(this.initialDestination); |  | ||||||
|       this.initialDestination = null; |  | ||||||
|     } else if (this.initialBookmark) { |  | ||||||
|       this.pdfLinkService.setHash(this.initialBookmark); |       this.pdfLinkService.setHash(this.initialBookmark); | ||||||
|       this.pdfHistory.push({ hash: this.initialBookmark, }, true); |  | ||||||
|       this.initialBookmark = null; |       this.initialBookmark = null; | ||||||
|     } else if (storedHash) { |     } else if (storedHash) { | ||||||
|       this.pdfLinkService.setHash(storedHash); |       this.pdfLinkService.setHash(storedHash); | ||||||
| @ -1787,10 +1777,6 @@ function webViewerUpdateViewarea(evt) { | |||||||
|   PDFViewerApplication.appConfig.secondaryToolbar.viewBookmarkButton.href = |   PDFViewerApplication.appConfig.secondaryToolbar.viewBookmarkButton.href = | ||||||
|     href; |     href; | ||||||
| 
 | 
 | ||||||
|   // Update the current bookmark in the browsing history.
 |  | ||||||
|   PDFViewerApplication.pdfHistory.updateCurrentBookmark(location.pdfOpenParams, |  | ||||||
|                                                         location.pageNumber); |  | ||||||
| 
 |  | ||||||
|   // Show/hide the loading indicator in the page number input element.
 |   // Show/hide the loading indicator in the page number input element.
 | ||||||
|   let currentPage = |   let currentPage = | ||||||
|     PDFViewerApplication.pdfViewer.getPageView(PDFViewerApplication.page - 1); |     PDFViewerApplication.pdfViewer.getPageView(PDFViewerApplication.page - 1); | ||||||
| @ -1814,17 +1800,15 @@ function webViewerResize() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function webViewerHashchange(evt) { | function webViewerHashchange(evt) { | ||||||
|   if (PDFViewerApplication.pdfHistory.isHashChangeUnlocked) { |  | ||||||
|   let hash = evt.hash; |   let hash = evt.hash; | ||||||
|   if (!hash) { |   if (!hash) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (!PDFViewerApplication.isInitialViewSet) { |   if (!PDFViewerApplication.isInitialViewSet) { | ||||||
|     PDFViewerApplication.initialBookmark = hash; |     PDFViewerApplication.initialBookmark = hash; | ||||||
|     } else { |   } else if (!PDFViewerApplication.pdfHistory.popStateInProgress) { | ||||||
|     PDFViewerApplication.pdfLinkService.setHash(hash); |     PDFViewerApplication.pdfLinkService.setHash(hash); | ||||||
|   } |   } | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| let webViewerFileInputChange; | let webViewerFileInputChange; | ||||||
| @ -2277,23 +2261,6 @@ function webViewerKeyDown(evt) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (cmd === 2) { // alt-key
 |  | ||||||
|     switch (evt.keyCode) { |  | ||||||
|       case 37: // left arrow
 |  | ||||||
|         if (isViewerInPresentationMode) { |  | ||||||
|           PDFViewerApplication.pdfHistory.back(); |  | ||||||
|           handled = true; |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
|       case 39: // right arrow
 |  | ||||||
|         if (isViewerInPresentationMode) { |  | ||||||
|           PDFViewerApplication.pdfHistory.forward(); |  | ||||||
|           handled = true; |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (ensureViewerFocused && !pdfViewer.containsElement(curElement)) { |   if (ensureViewerFocused && !pdfViewer.containsElement(curElement)) { | ||||||
|     // The page container is not focused, but a page navigation key has been
 |     // The page container is not focused, but a page navigation key has been
 | ||||||
|     // pressed. Change the focus to the viewer container to make sure that
 |     // pressed. Change the focus to the viewer container to make sure that
 | ||||||
|  | |||||||
| @ -73,10 +73,22 @@ class IPDFLinkService { | |||||||
|  * @interface |  * @interface | ||||||
|  */ |  */ | ||||||
| class IPDFHistory { | class IPDFHistory { | ||||||
|   forward() {} |   /** | ||||||
|  |    * @param {string} fingerprint - The PDF document's unique fingerprint. | ||||||
|  |    * @param {boolean} resetHistory - (optional) Reset the browsing history. | ||||||
|  |    */ | ||||||
|  |   initialize(fingerprint, resetHistory = false) {} | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @param {Object} params | ||||||
|  |    */ | ||||||
|  |   push({ namedDest, explicitDest, pageNumber, }) {} | ||||||
|  | 
 | ||||||
|  |   pushCurrentPosition() {} | ||||||
|  | 
 | ||||||
|   back() {} |   back() {} | ||||||
|   push(params) {} | 
 | ||||||
|   updateNextHashParam(hash) {} |   forward() {} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| /* Copyright 2012 Mozilla Foundation | /* Copyright 2017 Mozilla Foundation | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
| @ -12,415 +12,579 @@ | |||||||
|  * See the License for the specific language governing permissions and |  * See the License for the specific language governing permissions and | ||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| /* globals chrome */ |  | ||||||
| 
 | 
 | ||||||
|  | import { cloneObj, parseQueryString, waitOnEventOrTimeout } from './ui_utils'; | ||||||
| import { getGlobalEventBus } from './dom_events'; | import { getGlobalEventBus } from './dom_events'; | ||||||
| 
 | 
 | ||||||
| function PDFHistory(options) { | // Heuristic value used when force-resetting `this._blockHashChange`.
 | ||||||
|   this.linkService = options.linkService; | const HASH_CHANGE_TIMEOUT = 1000; // milliseconds
 | ||||||
|   this.eventBus = options.eventBus || getGlobalEventBus(); | // Heuristic value used when adding the current position to the browser history.
 | ||||||
|  | const POSITION_UPDATED_THRESHOLD = 50; | ||||||
|  | // Heuristic value used when adding a temporary position to the browser history.
 | ||||||
|  | const UPDATE_VIEWAREA_TIMEOUT = 2000; // milliseconds
 | ||||||
| 
 | 
 | ||||||
|   this.initialized = false; | /** | ||||||
|   this.initialDestination = null; |  * @typedef {Object} PDFHistoryOptions | ||||||
|   this.initialBookmark = null; |  * @property {IPDFLinkService} linkService - The navigation/linking service. | ||||||
|  |  * @property {EventBus} eventBus - The application event bus. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @typedef {Object} PushParameters | ||||||
|  |  * @property {string} namedDest - (optional) The named destination. If absent, | ||||||
|  |  *   a stringified version of `explicitDest` is used. | ||||||
|  |  * @property {Array} explicitDest - The explicit destination array. | ||||||
|  |  * @property {number} pageNumber - The page to which the destination points. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | function getCurrentHash() { | ||||||
|  |   return document.location.hash; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| PDFHistory.prototype = { | function parseCurrentHash(linkService) { | ||||||
|  |   let hash = unescape(getCurrentHash()).substring(1); | ||||||
|  |   let params = parseQueryString(hash); | ||||||
|  | 
 | ||||||
|  |   let page = params.page | 0; | ||||||
|  |   if (!(Number.isInteger(page) && page > 0 && page <= linkService.pagesCount)) { | ||||||
|  |     page = null; | ||||||
|  |   } | ||||||
|  |   return { hash, page, }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class PDFHistory { | ||||||
|   /** |   /** | ||||||
|    * @param {string} fingerprint |    * @param {PDFHistoryOptions} options | ||||||
|    */ |    */ | ||||||
|   initialize: function pdfHistoryInitialize(fingerprint) { |   constructor({ linkService, eventBus, }) { | ||||||
|     this.initialized = true; |     this.linkService = linkService; | ||||||
|     this.reInitialized = false; |     this.eventBus = eventBus || getGlobalEventBus(); | ||||||
|     this.allowHashChange = true; |  | ||||||
|     this.historyUnlocked = true; |  | ||||||
|     this.isViewerInPresentationMode = false; |  | ||||||
| 
 | 
 | ||||||
|     this.previousHash = window.location.hash.substring(1); |     this.initialized = false; | ||||||
|     this.currentBookmark = ''; |     this.initialBookmark = null; | ||||||
|     this.currentPage = 0; |  | ||||||
|     this.updatePreviousBookmark = false; |  | ||||||
|     this.previousBookmark = ''; |  | ||||||
|     this.previousPage = 0; |  | ||||||
|     this.nextHashParam = ''; |  | ||||||
| 
 | 
 | ||||||
|  |     this._boundEvents = Object.create(null); | ||||||
|  |     this._isViewerInPresentationMode = false; | ||||||
|  |     this._isPagesLoaded = false; | ||||||
|  | 
 | ||||||
|  |     // Ensure that we don't miss either a 'presentationmodechanged' or a
 | ||||||
|  |     // 'pagesloaded' event, by registering the listeners immediately.
 | ||||||
|  |     this.eventBus.on('presentationmodechanged', (evt) => { | ||||||
|  |       this._isViewerInPresentationMode = evt.active || evt.switchInProgress; | ||||||
|  |     }); | ||||||
|  |     this.eventBus.on('pagesloaded', (evt) => { | ||||||
|  |       this._isPagesLoaded = !!evt.pagesCount; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Initialize the history for the PDF document, using either the current | ||||||
|  |    * browser history entry or the document hash, whichever is present. | ||||||
|  |    * @param {string} fingerprint - The PDF document's unique fingerprint. | ||||||
|  |    * @param {boolean} resetHistory - (optional) Reset the browsing history. | ||||||
|  |    */ | ||||||
|  |   initialize(fingerprint, resetHistory = false) { | ||||||
|  |     if (!fingerprint || typeof fingerprint !== 'string') { | ||||||
|  |       console.error( | ||||||
|  |         'PDFHistory.initialize: The "fingerprint" must be a non-empty string.'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     let reInitialized = this.initialized && this.fingerprint !== fingerprint; | ||||||
|     this.fingerprint = fingerprint; |     this.fingerprint = fingerprint; | ||||||
|     this.currentUid = this.uid = 0; |  | ||||||
|     this.current = {}; |  | ||||||
| 
 | 
 | ||||||
|     var state = window.history.state; |  | ||||||
|     if (this._isStateObjectDefined(state)) { |  | ||||||
|       // This corresponds to navigating back to the document
 |  | ||||||
|       // from another page in the browser history.
 |  | ||||||
|       if (state.target.dest) { |  | ||||||
|         this.initialDestination = state.target.dest; |  | ||||||
|       } else { |  | ||||||
|         this.initialBookmark = state.target.hash; |  | ||||||
|       } |  | ||||||
|       this.currentUid = state.uid; |  | ||||||
|       this.uid = state.uid + 1; |  | ||||||
|       this.current = state.target; |  | ||||||
|     } else { |  | ||||||
|       // This corresponds to the loading of a new document.
 |  | ||||||
|       if (state && state.fingerprint && |  | ||||||
|         this.fingerprint !== state.fingerprint) { |  | ||||||
|         // Reinitialize the browsing history when a new document
 |  | ||||||
|         // is opened in the web viewer.
 |  | ||||||
|         this.reInitialized = true; |  | ||||||
|       } |  | ||||||
|       this._pushOrReplaceState({ fingerprint: this.fingerprint, }, true); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var self = this; |  | ||||||
|     window.addEventListener('popstate', function pdfHistoryPopstate(evt) { |  | ||||||
|       if (!self.historyUnlocked) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (evt.state) { |  | ||||||
|         // Move back/forward in the history.
 |  | ||||||
|         self._goTo(evt.state); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // If the state is not set, then the user tried to navigate to a
 |  | ||||||
|       // different hash by manually editing the URL and pressing Enter, or by
 |  | ||||||
|       // clicking on an in-page link (e.g. the "current view" link).
 |  | ||||||
|       // Save the current view state to the browser history.
 |  | ||||||
| 
 |  | ||||||
|       // Note: In Firefox, history.null could also be null after an in-page
 |  | ||||||
|       // navigation to the same URL, and without dispatching the popstate
 |  | ||||||
|       // event: https://bugzilla.mozilla.org/show_bug.cgi?id=1183881
 |  | ||||||
| 
 |  | ||||||
|       if (self.uid === 0) { |  | ||||||
|         // Replace the previous state if it was not explicitly set.
 |  | ||||||
|         var previousParams = (self.previousHash && self.currentBookmark && |  | ||||||
|           self.previousHash !== self.currentBookmark) ? |  | ||||||
|           { hash: self.currentBookmark, page: self.currentPage, } : |  | ||||||
|           { page: 1, }; |  | ||||||
|         replacePreviousHistoryState(previousParams, function() { |  | ||||||
|           updateHistoryWithCurrentHash(); |  | ||||||
|         }); |  | ||||||
|       } else { |  | ||||||
|         updateHistoryWithCurrentHash(); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     function updateHistoryWithCurrentHash() { |  | ||||||
|       self.previousHash = window.location.hash.slice(1); |  | ||||||
|       self._pushToHistory({ hash: self.previousHash, }, false, true); |  | ||||||
|       self._updatePreviousBookmark(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function replacePreviousHistoryState(params, callback) { |  | ||||||
|       // To modify the previous history entry, the following happens:
 |  | ||||||
|       // 1. history.back()
 |  | ||||||
|       // 2. _pushToHistory, which calls history.replaceState( ... )
 |  | ||||||
|       // 3. history.forward()
 |  | ||||||
|       // Because a navigation via the history API does not immediately update
 |  | ||||||
|       // the history state, the popstate event is used for synchronization.
 |  | ||||||
|       self.historyUnlocked = false; |  | ||||||
| 
 |  | ||||||
|       // Suppress the hashchange event to avoid side effects caused by
 |  | ||||||
|       // navigating back and forward.
 |  | ||||||
|       self.allowHashChange = false; |  | ||||||
|       window.addEventListener('popstate', rewriteHistoryAfterBack); |  | ||||||
|       history.back(); |  | ||||||
| 
 |  | ||||||
|       function rewriteHistoryAfterBack() { |  | ||||||
|         window.removeEventListener('popstate', rewriteHistoryAfterBack); |  | ||||||
|         window.addEventListener('popstate', rewriteHistoryAfterForward); |  | ||||||
|         self._pushToHistory(params, false, true); |  | ||||||
|         history.forward(); |  | ||||||
|       } |  | ||||||
|       function rewriteHistoryAfterForward() { |  | ||||||
|         window.removeEventListener('popstate', rewriteHistoryAfterForward); |  | ||||||
|         self.allowHashChange = true; |  | ||||||
|         self.historyUnlocked = true; |  | ||||||
|         callback(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function pdfHistoryBeforeUnload() { |  | ||||||
|       var previousParams = self._getPreviousParams(null, true); |  | ||||||
|       if (previousParams) { |  | ||||||
|         var replacePrevious = (!self.current.dest && |  | ||||||
|         self.current.hash !== self.previousHash); |  | ||||||
|         self._pushToHistory(previousParams, false, replacePrevious); |  | ||||||
|         self._updatePreviousBookmark(); |  | ||||||
|       } |  | ||||||
|       // Remove the event listener when navigating away from the document,
 |  | ||||||
|       // since 'beforeunload' prevents Firefox from caching the document.
 |  | ||||||
|       window.removeEventListener('beforeunload', pdfHistoryBeforeUnload); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     window.addEventListener('beforeunload', pdfHistoryBeforeUnload); |  | ||||||
| 
 |  | ||||||
|     window.addEventListener('pageshow', function pdfHistoryPageShow(evt) { |  | ||||||
|       // If the entire viewer (including the PDF file) is cached in
 |  | ||||||
|       // the browser, we need to reattach the 'beforeunload' event listener
 |  | ||||||
|       // since the 'DOMContentLoaded' event is not fired on 'pageshow'.
 |  | ||||||
|       window.addEventListener('beforeunload', pdfHistoryBeforeUnload); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     self.eventBus.on('presentationmodechanged', function(e) { |  | ||||||
|       self.isViewerInPresentationMode = e.active; |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   clearHistoryState: function pdfHistory_clearHistoryState() { |  | ||||||
|     this._pushOrReplaceState(null, true); |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) { |  | ||||||
|     return (state && state.uid >= 0 && |  | ||||||
|     state.fingerprint && this.fingerprint === state.fingerprint && |  | ||||||
|     state.target && state.target.hash) ? true : false; |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj, |  | ||||||
|                                                               replace) { |  | ||||||
|     // history.state.chromecomState is managed by chromecom.js.
 |  | ||||||
|     if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME') && |  | ||||||
|         window.history.state && 'chromecomState' in window.history.state) { |  | ||||||
|       stateObj = stateObj || {}; |  | ||||||
|       stateObj.chromecomState = window.history.state.chromecomState; |  | ||||||
|     } |  | ||||||
|     if (replace) { |  | ||||||
|       if (typeof PDFJSDev === 'undefined' || |  | ||||||
|           PDFJSDev.test('GENERIC || CHROME')) { |  | ||||||
|         window.history.replaceState(stateObj, '', document.URL); |  | ||||||
|       } else { |  | ||||||
|         window.history.replaceState(stateObj, ''); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       if (typeof PDFJSDev === 'undefined' || |  | ||||||
|           PDFJSDev.test('GENERIC || CHROME')) { |  | ||||||
|         window.history.pushState(stateObj, '', document.URL); |  | ||||||
|       } else { |  | ||||||
|         window.history.pushState(stateObj, ''); |  | ||||||
|       } |  | ||||||
|       if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME') && |  | ||||||
|           top === window) { |  | ||||||
|         chrome.runtime.sendMessage('showPageAction'); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   get isHashChangeUnlocked() { |  | ||||||
|     if (!this.initialized) { |     if (!this.initialized) { | ||||||
|       return true; |       this._bindEvents(); | ||||||
|     } |     } | ||||||
|     return this.allowHashChange; |     let state = window.history.state; | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() { |     this.initialized = true; | ||||||
|     if (this.updatePreviousBookmark && |     this.initialBookmark = null; | ||||||
|       this.currentBookmark && this.currentPage) { |  | ||||||
|       this.previousBookmark = this.currentBookmark; |  | ||||||
|       this.previousPage = this.currentPage; |  | ||||||
|       this.updatePreviousBookmark = false; |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark, |     this._popStateInProgress = false; | ||||||
|                                                                   pageNum) { |     this._blockHashChange = 0; | ||||||
|     if (this.initialized) { |     this._currentHash = getCurrentHash(); | ||||||
|       this.currentBookmark = bookmark.substring(1); |     this._numPositionUpdates = 0; | ||||||
|       this.currentPage = pageNum | 0; |  | ||||||
|       this._updatePreviousBookmark(); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) { |     this._currentUid = this._uid = 0; | ||||||
|     if (this.initialized) { |     this._destination = null; | ||||||
|       this.nextHashParam = param; |     this._position = null; | ||||||
|     } |  | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   push: function pdfHistoryPush(params, isInitialBookmark) { |     if (!this._isValidState(state) || resetHistory) { | ||||||
|     if (!(this.initialized && this.historyUnlocked)) { |       let { hash, page, } = parseCurrentHash(this.linkService); | ||||||
|  | 
 | ||||||
|  |       if (!hash || reInitialized || resetHistory) { | ||||||
|  |         // Ensure that the browser history is reset on PDF document load.
 | ||||||
|  |         this._pushOrReplaceState(null, /* forceReplace = */ true); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|     if (params.dest && !params.hash) { |       // Ensure that the browser history is initialized correctly when
 | ||||||
|       params.hash = (this.current.hash && this.current.dest && |       // the document hash is present on PDF document load.
 | ||||||
|       this.current.dest === params.dest) ? |       this._pushOrReplaceState({ hash, page, }, /* forceReplace = */ true); | ||||||
|         this.current.hash : |  | ||||||
|         this.linkService.getDestinationHash(params.dest).split('#')[1]; |  | ||||||
|     } |  | ||||||
|     if (params.page) { |  | ||||||
|       params.page |= 0; |  | ||||||
|     } |  | ||||||
|     if (isInitialBookmark) { |  | ||||||
|       var target = window.history.state.target; |  | ||||||
|       if (!target) { |  | ||||||
|         // Invoked when the user specifies an initial bookmark,
 |  | ||||||
|         // thus setting initialBookmark, when the document is loaded.
 |  | ||||||
|         this._pushToHistory(params, false); |  | ||||||
|         this.previousHash = window.location.hash.substring(1); |  | ||||||
|       } |  | ||||||
|       this.updatePreviousBookmark = this.nextHashParam ? false : true; |  | ||||||
|       if (target) { |  | ||||||
|         // If the current document is reloaded,
 |  | ||||||
|         // avoid creating duplicate entries in the history.
 |  | ||||||
|         this._updatePreviousBookmark(); |  | ||||||
|       } |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if (this.nextHashParam) { | 
 | ||||||
|       if (this.nextHashParam === params.hash) { |     // The browser history contains a valid entry, ensure that the history is
 | ||||||
|         this.nextHashParam = null; |     // initialized correctly on PDF document load.
 | ||||||
|         this.updatePreviousBookmark = true; |     let destination = state.destination; | ||||||
|         return; |     this._updateInternalState(destination, state.uid, | ||||||
|  |                               /* removeTemporary = */ true); | ||||||
|  |     if (destination.dest) { | ||||||
|  |       this.initialBookmark = JSON.stringify(destination.dest); | ||||||
|  | 
 | ||||||
|  |       // If the history is updated, e.g. through the user changing the hash,
 | ||||||
|  |       // before the initial destination has become visible, then we do *not*
 | ||||||
|  |       // want to potentially add `this._position` to the browser history.
 | ||||||
|  |       this._destination.page = null; | ||||||
|  |     } else if (destination.hash) { | ||||||
|  |       this.initialBookmark = destination.hash; | ||||||
|  |     } else if (destination.page) { | ||||||
|  |       // Fallback case; shouldn't be necessary, but better safe than sorry.
 | ||||||
|  |       this.initialBookmark = `page=${destination.page}`; | ||||||
|     } |     } | ||||||
|       this.nextHashParam = null; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|     if (params.hash) { |   /** | ||||||
|       if (this.current.hash) { |    * Push an internal destination to the browser history. | ||||||
|         if (this.current.hash !== params.hash) { |    * @param {PushParameters} | ||||||
|           this._pushToHistory(params, true); |    */ | ||||||
|         } else { |   push({ namedDest, explicitDest, pageNumber, }) { | ||||||
|           if (!this.current.page && params.page) { |  | ||||||
|             this._pushToHistory(params, false, true); |  | ||||||
|           } |  | ||||||
|           this.updatePreviousBookmark = true; |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         this._pushToHistory(params, true); |  | ||||||
|       } |  | ||||||
|     } else if (this.current.page && params.page && |  | ||||||
|       this.current.page !== params.page) { |  | ||||||
|       this._pushToHistory(params, true); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage, |  | ||||||
|                                                             beforeUnload) { |  | ||||||
|     if (!(this.currentBookmark && this.currentPage)) { |  | ||||||
|       return null; |  | ||||||
|     } else if (this.updatePreviousBookmark) { |  | ||||||
|       this.updatePreviousBookmark = false; |  | ||||||
|     } |  | ||||||
|     if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) { |  | ||||||
|       // Prevent the history from getting stuck in the current state,
 |  | ||||||
|       // effectively preventing the user from going back/forward in
 |  | ||||||
|       // the history.
 |  | ||||||
|       //
 |  | ||||||
|       // This happens if the current position in the document didn't change
 |  | ||||||
|       // when the history was previously updated. The reasons for this are
 |  | ||||||
|       // either:
 |  | ||||||
|       // 1. The current zoom value is such that the document does not need to,
 |  | ||||||
|       //    or cannot, be scrolled to display the destination.
 |  | ||||||
|       // 2. The previous destination is broken, and doesn't actally point to a
 |  | ||||||
|       //    position within the document.
 |  | ||||||
|       //    (This is either due to a bad PDF generator, or the user making a
 |  | ||||||
|       //     mistake when entering a destination in the hash parameters.)
 |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|     if ((!this.current.dest && !onlyCheckPage) || beforeUnload) { |  | ||||||
|       if (this.previousBookmark === this.currentBookmark) { |  | ||||||
|         return null; |  | ||||||
|       } |  | ||||||
|     } else if (this.current.page || onlyCheckPage) { |  | ||||||
|       if (this.previousPage === this.currentPage) { |  | ||||||
|         return null; |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|     var params = { hash: this.currentBookmark, page: this.currentPage, }; |  | ||||||
|     if (this.isViewerInPresentationMode) { |  | ||||||
|       params.hash = null; |  | ||||||
|     } |  | ||||||
|     return params; |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   _stateObj: function pdfHistory_stateObj(params) { |  | ||||||
|     return { fingerprint: this.fingerprint, uid: this.uid, target: params, }; |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   _pushToHistory: function pdfHistory_pushToHistory(params, |  | ||||||
|                                                     addPrevious, overwrite) { |  | ||||||
|     if (!this.initialized) { |     if (!this.initialized) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if (!params.hash && params.page) { |     if ((namedDest && typeof namedDest !== 'string') || | ||||||
|       params.hash = ('page=' + params.page); |         !(explicitDest instanceof Array) || | ||||||
|     } |         !(Number.isInteger(pageNumber) && | ||||||
|     if (addPrevious && !overwrite) { |           pageNumber > 0 && pageNumber <= this.linkService.pagesCount)) { | ||||||
|       var previousParams = this._getPreviousParams(); |       console.error('PDFHistory.push: Invalid parameters.'); | ||||||
|       if (previousParams) { |  | ||||||
|         var replacePrevious = (!this.current.dest && |  | ||||||
|         this.current.hash !== this.previousHash); |  | ||||||
|         this._pushToHistory(previousParams, false, replacePrevious); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     this._pushOrReplaceState(this._stateObj(params), |  | ||||||
|       (overwrite || this.uid === 0)); |  | ||||||
|     this.currentUid = this.uid++; |  | ||||||
|     this.current = params; |  | ||||||
|     this.updatePreviousBookmark = true; |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   _goTo: function pdfHistory_goTo(state) { |  | ||||||
|     if (!(this.initialized && this.historyUnlocked && |  | ||||||
|       this._isStateObjectDefined(state))) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if (!this.reInitialized && state.uid < this.currentUid) { | 
 | ||||||
|       var previousParams = this._getPreviousParams(true); |     let hash = namedDest || JSON.stringify(explicitDest); | ||||||
|       if (previousParams) { |     if (!hash) { | ||||||
|         this._pushToHistory(this.current, false); |       // The hash *should* never be undefined, but if that were to occur,
 | ||||||
|         this._pushToHistory(previousParams, false); |       // avoid any possible issues by not updating the browser history.
 | ||||||
|         this.currentUid = state.uid; |  | ||||||
|         window.history.back(); |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     let forceReplace = false; | ||||||
|  |     if (this._destination && | ||||||
|  |         (this._destination.hash === hash || | ||||||
|  |          isDestsEqual(this._destination.dest, explicitDest))) { | ||||||
|  |       // When the new destination is identical to `this._destination`, and
 | ||||||
|  |       // its `page` is undefined, replace the current browser history entry.
 | ||||||
|  |       // NOTE: This can only occur if `this._destination` was set either:
 | ||||||
|  |       //  - through the document hash being specified on load.
 | ||||||
|  |       //  - through the user changing the hash of the document.
 | ||||||
|  |       if (this._destination.page) { | ||||||
|  |         return; | ||||||
|       } |       } | ||||||
|     this.historyUnlocked = false; |       forceReplace = true; | ||||||
| 
 |  | ||||||
|     if (state.target.dest) { |  | ||||||
|       this.linkService.navigateTo(state.target.dest); |  | ||||||
|     } else { |  | ||||||
|       this.linkService.setHash(state.target.hash); |  | ||||||
|     } |     } | ||||||
|     this.currentUid = state.uid; |     if (this._popStateInProgress && !forceReplace) { | ||||||
|     if (state.uid > this.uid) { |       return; | ||||||
|       this.uid = state.uid; |  | ||||||
|     } |     } | ||||||
|     this.current = state.target; |  | ||||||
|     this.updatePreviousBookmark = true; |  | ||||||
| 
 | 
 | ||||||
|     var currentHash = window.location.hash.substring(1); |     this._pushOrReplaceState({ | ||||||
|     if (this.previousHash !== currentHash) { |       dest: explicitDest, | ||||||
|       this.allowHashChange = false; |       hash, | ||||||
|  |       page: pageNumber, | ||||||
|  |     }, forceReplace); | ||||||
|   } |   } | ||||||
|     this.previousHash = currentHash; |  | ||||||
| 
 | 
 | ||||||
|     this.historyUnlocked = true; |   /** | ||||||
|   }, |    * Push the current position to the browser history. | ||||||
|  |    */ | ||||||
|  |   pushCurrentPosition() { | ||||||
|  |     if (!this.initialized || this._popStateInProgress) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._tryPushCurrentPosition(); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   back: function pdfHistoryBack() { |   /** | ||||||
|     this.go(-1); |    * Go back one step in the browser history. | ||||||
|   }, |    * NOTE: Avoids navigating away from the document, useful for "named actions". | ||||||
| 
 |    */ | ||||||
|   forward: function pdfHistoryForward() { |   back() { | ||||||
|     this.go(1); |     if (!this.initialized || this._popStateInProgress) { | ||||||
|   }, |       return; | ||||||
| 
 |     } | ||||||
|   go: function pdfHistoryGo(direction) { |     let state = window.history.state; | ||||||
|     if (this.initialized && this.historyUnlocked) { |     if (this._isValidState(state) && state.uid > 0) { | ||||||
|       var state = window.history.state; |  | ||||||
|       if (direction === -1 && state && state.uid > 0) { |  | ||||||
|       window.history.back(); |       window.history.back(); | ||||||
|       } else if (direction === 1 && state && state.uid < (this.uid - 1)) { |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Go forward one step in the browser history. | ||||||
|  |    * NOTE: Avoids navigating away from the document, useful for "named actions". | ||||||
|  |    */ | ||||||
|  |   forward() { | ||||||
|  |     if (!this.initialized || this._popStateInProgress) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     let state = window.history.state; | ||||||
|  |     if (this._isValidState(state) && state.uid < (this._uid - 1)) { | ||||||
|       window.history.forward(); |       window.history.forward(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   }, | 
 | ||||||
| }; |   /** | ||||||
|  |    * @returns {boolean} Indicating if the user is currently moving through the | ||||||
|  |    *   browser history, useful e.g. for skipping the next 'hashchange' event. | ||||||
|  |    */ | ||||||
|  |   get popStateInProgress() { | ||||||
|  |     return this.initialized && | ||||||
|  |            (this._popStateInProgress || this._blockHashChange > 0); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _pushOrReplaceState(destination, forceReplace = false) { | ||||||
|  |     let shouldReplace = forceReplace || !this._destination; | ||||||
|  |     let newState = { | ||||||
|  |       fingerprint: this.fingerprint, | ||||||
|  |       uid: shouldReplace ? this._currentUid : this._uid, | ||||||
|  |       destination, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME') && | ||||||
|  |         window.history.state && window.history.state.chromecomState) { | ||||||
|  |       // history.state.chromecomState is managed by chromecom.js.
 | ||||||
|  |       newState.chromecomState = window.history.state.chromecomState; | ||||||
|  |     } | ||||||
|  |     this._updateInternalState(destination, newState.uid); | ||||||
|  | 
 | ||||||
|  |     if (shouldReplace) { | ||||||
|  |       if (typeof PDFJSDev !== 'undefined' && | ||||||
|  |           PDFJSDev.test('FIREFOX || MOZCENTRAL')) { | ||||||
|  |         // Providing the third argument causes a SecurityError for file:// URLs.
 | ||||||
|  |         window.history.replaceState(newState, ''); | ||||||
|  |       } else { | ||||||
|  |         window.history.replaceState(newState, '', document.URL); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       if (typeof PDFJSDev !== 'undefined' && | ||||||
|  |           PDFJSDev.test('FIREFOX || MOZCENTRAL')) { | ||||||
|  |         // Providing the third argument causes a SecurityError for file:// URLs.
 | ||||||
|  |         window.history.pushState(newState, ''); | ||||||
|  |       } else { | ||||||
|  |         window.history.pushState(newState, '', document.URL); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME') && | ||||||
|  |         top === window) { | ||||||
|  |       // eslint-disable-next-line no-undef
 | ||||||
|  |       chrome.runtime.sendMessage('showPageAction'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _tryPushCurrentPosition(temporary = false) { | ||||||
|  |     if (!this._position) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     let position = this._position; | ||||||
|  |     if (temporary) { | ||||||
|  |       position = cloneObj(this._position); | ||||||
|  |       position.temporary = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!this._destination) { | ||||||
|  |       this._pushOrReplaceState(position); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (this._destination.temporary) { | ||||||
|  |       // Always replace a previous *temporary* position.
 | ||||||
|  |       this._pushOrReplaceState(position, /* forceReplace = */ true); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (this._destination.hash === position.hash) { | ||||||
|  |       return; // The current document position has not changed.
 | ||||||
|  |     } | ||||||
|  |     if (!this._destination.page && | ||||||
|  |         (POSITION_UPDATED_THRESHOLD <= 0 || | ||||||
|  |          this._numPositionUpdates <= POSITION_UPDATED_THRESHOLD)) { | ||||||
|  |       // `this._destination` was set through the user changing the hash of
 | ||||||
|  |       // the document. Do not add `this._position` to the browser history,
 | ||||||
|  |       // to avoid "flooding" it with lots of (nearly) identical entries,
 | ||||||
|  |       // since we cannot ensure that the document position has changed.
 | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let forceReplace = false; | ||||||
|  |     if (this._destination.page === position.first || | ||||||
|  |         this._destination.page === position.page) { | ||||||
|  |       // When the `page` of `this._destination` is still visible, do not
 | ||||||
|  |       // update the browsing history when `this._destination` either:
 | ||||||
|  |       //  - contains an internal destination, since in this case we
 | ||||||
|  |       //    cannot ensure that the document position has actually changed.
 | ||||||
|  |       //  - was set through the user changing the hash of the document.
 | ||||||
|  |       if (this._destination.dest || !this._destination.first) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       // To avoid "flooding" the browser history, replace the current entry.
 | ||||||
|  |       forceReplace = true; | ||||||
|  |     } | ||||||
|  |     this._pushOrReplaceState(position, forceReplace); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _isValidState(state) { | ||||||
|  |     if (!state) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (state.fingerprint !== this.fingerprint) { | ||||||
|  |       // This should only occur in viewers with support for opening more than
 | ||||||
|  |       // one PDF document, e.g. the GENERIC viewer.
 | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (!Number.isInteger(state.uid) || state.uid < 0) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (state.destination === null || typeof state.destination !== 'object') { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _updateInternalState(destination, uid, removeTemporary = false) { | ||||||
|  |     if (removeTemporary && destination && destination.temporary) { | ||||||
|  |       // When the `destination` comes from the browser history,
 | ||||||
|  |       // we no longer treat it as a *temporary* position.
 | ||||||
|  |       delete destination.temporary; | ||||||
|  |     } | ||||||
|  |     this._destination = destination; | ||||||
|  |     this._currentUid = uid; | ||||||
|  |     this._uid = this._currentUid + 1; | ||||||
|  |     // This should always be reset when `this._destination` is updated.
 | ||||||
|  |     this._numPositionUpdates = 0; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _updateViewarea({ location, }) { | ||||||
|  |     if (this._updateViewareaTimeout) { | ||||||
|  |       clearTimeout(this._updateViewareaTimeout); | ||||||
|  |       this._updateViewareaTimeout = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this._position = { | ||||||
|  |       hash: this._isViewerInPresentationMode ? | ||||||
|  |         `page=${location.pageNumber}` : location.pdfOpenParams.substring(1), | ||||||
|  |       page: this.linkService.page, | ||||||
|  |       first: location.pageNumber, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     if (this._popStateInProgress) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (POSITION_UPDATED_THRESHOLD > 0 && this._isPagesLoaded && | ||||||
|  |         this._destination && !this._destination.page) { | ||||||
|  |       // If the current destination was set through the user changing the hash
 | ||||||
|  |       // of the document, we will usually not try to push the current position
 | ||||||
|  |       // to the browser history; see `this._tryPushCurrentPosition()`.
 | ||||||
|  |       //
 | ||||||
|  |       // To prevent `this._tryPushCurrentPosition()` from effectively being
 | ||||||
|  |       // reduced to a no-op in this case, we will assume that the position
 | ||||||
|  |       // *did* in fact change if the 'updateviewarea' event was dispatched
 | ||||||
|  |       // more than `POSITION_UPDATED_THRESHOLD` times.
 | ||||||
|  |       this._numPositionUpdates++; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (UPDATE_VIEWAREA_TIMEOUT > 0) { | ||||||
|  |       // When closing the browser, a 'pagehide' event will be dispatched which
 | ||||||
|  |       // *should* allow us to push the current position to the browser history.
 | ||||||
|  |       // In practice, it seems that the event is arriving too late in order for
 | ||||||
|  |       // the session history to be successfully updated.
 | ||||||
|  |       // (For additional details, please refer to the discussion in
 | ||||||
|  |       //  https://bugzilla.mozilla.org/show_bug.cgi?id=1153393.)
 | ||||||
|  |       //
 | ||||||
|  |       // To workaround this we attempt to *temporarily* add the current position
 | ||||||
|  |       // to the browser history only when the viewer is *idle*,
 | ||||||
|  |       // i.e. when scrolling and/or zooming does not occur.
 | ||||||
|  |       //
 | ||||||
|  |       // PLEASE NOTE: It's absolutely imperative that the browser history is
 | ||||||
|  |       // *not* updated too often, since that would render the viewer more or
 | ||||||
|  |       // less unusable. Hence the use of a timeout to delay the update until
 | ||||||
|  |       // the viewer has been idle for `UPDATE_VIEWAREA_TIMEOUT` milliseconds.
 | ||||||
|  |       this._updateViewareaTimeout = setTimeout(() => { | ||||||
|  |         if (!this._popStateInProgress) { | ||||||
|  |           this._tryPushCurrentPosition(/* temporary = */ true); | ||||||
|  |         } | ||||||
|  |         this._updateViewareaTimeout = null; | ||||||
|  |       }, UPDATE_VIEWAREA_TIMEOUT); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _popState({ state, }) { | ||||||
|  |     let newHash = getCurrentHash(), hashChanged = this._currentHash !== newHash; | ||||||
|  |     this._currentHash = newHash; | ||||||
|  | 
 | ||||||
|  |     if (!state || | ||||||
|  |         (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME') && | ||||||
|  |          state.chromecomState && !this._isValidState(state))) { | ||||||
|  |       // This case corresponds to the user changing the hash of the document.
 | ||||||
|  |       this._currentUid = this._uid; | ||||||
|  | 
 | ||||||
|  |       let { hash, page, } = parseCurrentHash(this.linkService); | ||||||
|  |       this._pushOrReplaceState({ hash, page, }, /* forceReplace */ true); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!this._isValidState(state)) { | ||||||
|  |       // This should only occur in viewers with support for opening more than
 | ||||||
|  |       // one PDF document, e.g. the GENERIC viewer.
 | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Prevent the browser history from updating until the new destination,
 | ||||||
|  |     // as stored in the browser history, has been scrolled into view.
 | ||||||
|  |     this._popStateInProgress = true; | ||||||
|  | 
 | ||||||
|  |     if (hashChanged) { | ||||||
|  |       // When the hash changed, implying that the 'popstate' event will be
 | ||||||
|  |       // followed by a 'hashchange' event, then we do *not* want to update the
 | ||||||
|  |       // browser history when handling the 'hashchange' event (in web/app.js)
 | ||||||
|  |       // since that would *overwrite* the new destination navigated to below.
 | ||||||
|  |       //
 | ||||||
|  |       // To avoid accidentally disabling all future user-initiated hash changes,
 | ||||||
|  |       // if there's e.g. another 'hashchange' listener that stops the event
 | ||||||
|  |       // propagation, we make sure to always force-reset `this._blockHashChange`
 | ||||||
|  |       // after `HASH_CHANGE_TIMEOUT` milliseconds have passed.
 | ||||||
|  |       this._blockHashChange++; | ||||||
|  |       waitOnEventOrTimeout({ | ||||||
|  |         target: window, | ||||||
|  |         name: 'hashchange', | ||||||
|  |         delay: HASH_CHANGE_TIMEOUT, | ||||||
|  |       }).then(() => { | ||||||
|  |         this._blockHashChange--; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // This case corresponds to navigation backwards in the browser history.
 | ||||||
|  |     if (state.uid < this._currentUid && this._position && this._destination) { | ||||||
|  |       let shouldGoBack = false; | ||||||
|  | 
 | ||||||
|  |       if (this._destination.temporary) { | ||||||
|  |         // If the `this._destination` contains a *temporary* position, always
 | ||||||
|  |         // push the `this._position` to the browser history before moving back.
 | ||||||
|  |         this._pushOrReplaceState(this._position); | ||||||
|  |         shouldGoBack = true; | ||||||
|  |       } else if (this._destination.page && | ||||||
|  |                  this._destination.page !== this._position.first && | ||||||
|  |                  this._destination.page !== this._position.page) { | ||||||
|  |         // If the `page` of the `this._destination` is no longer visible,
 | ||||||
|  |         // push the `this._position` to the browser history before moving back.
 | ||||||
|  |         this._pushOrReplaceState(this._destination); | ||||||
|  |         this._pushOrReplaceState(this._position); | ||||||
|  |         shouldGoBack = true; | ||||||
|  |       } | ||||||
|  |       if (shouldGoBack) { | ||||||
|  |         // After `window.history.back()`, we must not enter this block on the
 | ||||||
|  |         // resulting 'popstate' event, since that may cause an infinite loop.
 | ||||||
|  |         this._currentUid = state.uid; | ||||||
|  | 
 | ||||||
|  |         window.history.back(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Navigate to the new destination.
 | ||||||
|  |     let destination = state.destination; | ||||||
|  |     this._updateInternalState(destination, state.uid, | ||||||
|  |                               /* removeTemporary = */ true); | ||||||
|  |     if (destination.dest) { | ||||||
|  |       this.linkService.navigateTo(destination.dest); | ||||||
|  |     } else if (destination.hash) { | ||||||
|  |       this.linkService.setHash(destination.hash); | ||||||
|  |     } else if (destination.page) { | ||||||
|  |       // Fallback case; shouldn't be necessary, but better safe than sorry.
 | ||||||
|  |       this.linkService.page = destination.page; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Since `PDFLinkService.navigateTo` is asynchronous, we thus defer the
 | ||||||
|  |     // resetting of `this._popStateInProgress` slightly.
 | ||||||
|  |     Promise.resolve().then(() => { | ||||||
|  |       this._popStateInProgress = false; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _bindEvents() { | ||||||
|  |     let { _boundEvents, eventBus, } = this; | ||||||
|  | 
 | ||||||
|  |     _boundEvents.updateViewarea = this._updateViewarea.bind(this); | ||||||
|  |     _boundEvents.popState = this._popState.bind(this); | ||||||
|  |     _boundEvents.pageHide = (evt) => { | ||||||
|  |       // Attempt to push the `this._position` into the browser history when
 | ||||||
|  |       // navigating away from the document. This is *only* done if the history
 | ||||||
|  |       // is currently empty, since otherwise an existing browser history entry
 | ||||||
|  |       // will end up being overwritten (given that new entries cannot be pushed
 | ||||||
|  |       // into the browser history when the 'unload' event has already fired).
 | ||||||
|  |       if (!this._destination) { | ||||||
|  |         this._tryPushCurrentPosition(); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     eventBus.on('updateviewarea', _boundEvents.updateViewarea); | ||||||
|  |     window.addEventListener('popstate', _boundEvents.popState); | ||||||
|  |     window.addEventListener('pagehide', _boundEvents.pageHide); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function isDestsEqual(firstDest, secondDest) { | ||||||
|  |   function isEntryEqual(first, second) { | ||||||
|  |     if (typeof first !== typeof second) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (first instanceof Array || second instanceof Array) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (first !== null && typeof first === 'object' && second !== null) { | ||||||
|  |       if (Object.keys(first).length !== Object.keys(second).length) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       for (var key in first) { | ||||||
|  |         if (!isEntryEqual(first[key], second[key])) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     return first === second || (Number.isNaN(first) && Number.isNaN(second)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!(firstDest instanceof Array && secondDest instanceof Array)) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   if (firstDest.length !== secondDest.length) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   for (let i = 0, ii = firstDest.length; i < ii; i++) { | ||||||
|  |     if (!isEntryEqual(firstDest[i], secondDest[i])) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export { | export { | ||||||
|   PDFHistory, |   PDFHistory, | ||||||
|  |   isDestsEqual, | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -111,18 +111,17 @@ class PDFLinkService { | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       if (this.pdfHistory) { | ||||||
|  |         // Update the browser history before scrolling the new destination into
 | ||||||
|  |         // view, to be able to accurately capture the current document position.
 | ||||||
|  |         this.pdfHistory.pushCurrentPosition(); | ||||||
|  |         this.pdfHistory.push({ namedDest, explicitDest, pageNumber, }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       this.pdfViewer.scrollPageIntoView({ |       this.pdfViewer.scrollPageIntoView({ | ||||||
|         pageNumber, |         pageNumber, | ||||||
|         destArray: explicitDest, |         destArray: explicitDest, | ||||||
|       }); |       }); | ||||||
| 
 |  | ||||||
|       if (this.pdfHistory) { // Update the browsing history, if enabled.
 |  | ||||||
|         this.pdfHistory.push({ |  | ||||||
|           dest: explicitDest, |  | ||||||
|           hash: namedDest, |  | ||||||
|           page: pageNumber, |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     new Promise((resolve, reject) => { |     new Promise((resolve, reject) => { | ||||||
| @ -190,9 +189,6 @@ class PDFLinkService { | |||||||
|       } |       } | ||||||
|       // borrowing syntax from "Parameters for Opening PDF Files"
 |       // borrowing syntax from "Parameters for Opening PDF Files"
 | ||||||
|       if ('nameddest' in params) { |       if ('nameddest' in params) { | ||||||
|         if (this.pdfHistory) { |  | ||||||
|           this.pdfHistory.updateNextHashParam(params.nameddest); |  | ||||||
|         } |  | ||||||
|         this.navigateTo(params.nameddest); |         this.navigateTo(params.nameddest); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| @ -270,9 +266,6 @@ class PDFLinkService { | |||||||
|       } catch (ex) {} |       } catch (ex) {} | ||||||
| 
 | 
 | ||||||
|       if (typeof dest === 'string' || isValidExplicitDestination(dest)) { |       if (typeof dest === 'string' || isValidExplicitDestination(dest)) { | ||||||
|         if (this.pdfHistory) { |  | ||||||
|           this.pdfHistory.updateNextHashParam(dest); |  | ||||||
|         } |  | ||||||
|         this.navigateTo(dest); |         this.navigateTo(dest); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import { PDFJS } from 'pdfjs-lib'; | import { createPromiseCapability, PDFJS } from 'pdfjs-lib'; | ||||||
| 
 | 
 | ||||||
| const CSS_UNITS = 96.0 / 72.0; | const CSS_UNITS = 96.0 / 72.0; | ||||||
| const DEFAULT_SCALE_VALUE = 'auto'; | const DEFAULT_SCALE_VALUE = 'auto'; | ||||||
| @ -453,6 +453,62 @@ function cloneObj(obj) { | |||||||
|   return result; |   return result; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const WaitOnType = { | ||||||
|  |   EVENT: 'event', | ||||||
|  |   TIMEOUT: 'timeout', | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @typedef {Object} WaitOnEventOrTimeoutParameters | ||||||
|  |  * @property {Object} target - The event target, can for example be: | ||||||
|  |  *   `window`, `document`, a DOM element, or an {EventBus} instance. | ||||||
|  |  * @property {string} name - The name of the event. | ||||||
|  |  * @property {number} delay - The delay, in milliseconds, after which the | ||||||
|  |  *   timeout occurs (if the event wasn't already dispatched). | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Allows waiting for an event or a timeout, whichever occurs first. | ||||||
|  |  * Can be used to ensure that an action always occurs, even when an event | ||||||
|  |  * arrives late or not at all. | ||||||
|  |  * | ||||||
|  |  * @param {WaitOnEventOrTimeoutParameters} | ||||||
|  |  * @returns {Promise} A promise that is resolved with a {WaitOnType} value. | ||||||
|  |  */ | ||||||
|  | function waitOnEventOrTimeout({ target, name, delay = 0, }) { | ||||||
|  |   if (typeof target !== 'object' || !(name && typeof name === 'string') || | ||||||
|  |       !(Number.isInteger(delay) && delay >= 0)) { | ||||||
|  |     return Promise.reject( | ||||||
|  |       new Error('waitOnEventOrTimeout - invalid paramaters.')); | ||||||
|  |   } | ||||||
|  |   let capability = createPromiseCapability(); | ||||||
|  | 
 | ||||||
|  |   function handler(type) { | ||||||
|  |     if (target instanceof EventBus) { | ||||||
|  |       target.off(name, eventHandler); | ||||||
|  |     } else { | ||||||
|  |       target.removeEventListener(name, eventHandler); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (timeout) { | ||||||
|  |       clearTimeout(timeout); | ||||||
|  |     } | ||||||
|  |     capability.resolve(type); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let eventHandler = handler.bind(null, WaitOnType.EVENT); | ||||||
|  |   if (target instanceof EventBus) { | ||||||
|  |     target.on(name, eventHandler); | ||||||
|  |   } else { | ||||||
|  |     target.addEventListener(name, eventHandler); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT); | ||||||
|  |   let timeout = setTimeout(timeoutHandler, delay); | ||||||
|  | 
 | ||||||
|  |   return capability.promise; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Promise that is resolved when DOM window becomes visible. |  * Promise that is resolved when DOM window becomes visible. | ||||||
|  */ |  */ | ||||||
| @ -618,4 +674,6 @@ export { | |||||||
|   normalizeWheelEventDelta, |   normalizeWheelEventDelta, | ||||||
|   animationStarted, |   animationStarted, | ||||||
|   localized, |   localized, | ||||||
|  |   WaitOnType, | ||||||
|  |   waitOnEventOrTimeout, | ||||||
| }; | }; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user