From 5fa9cca8dd1d213b6dd0be9ee6ea911deb80b98a Mon Sep 17 00:00:00 2001
From: Jonas Jenwald <jonas.jenwald@gmail.com>
Date: Tue, 1 Aug 2017 14:11:28 +0200
Subject: [PATCH] Refactor `PDFViewer` to extend an abstract `BaseViewer` class

This patch introduces an abstract `BaseViewer` class, that the existing `PDFViewer` then extends. *Please note:* This lays the necessary foundation for the next patch.
---
 web/app.js                  |   4 +-
 web/base_viewer.js          | 119 +++++++++++++-----------------------
 web/pdf_viewer.component.js |   2 +-
 web/pdf_viewer.js           |  83 +++++++++++++++++++++++++
 web/ui_utils.js             |   8 +++
 5 files changed, 138 insertions(+), 78 deletions(-)
 create mode 100644 web/pdf_viewer.js

diff --git a/web/app.js b/web/app.js
index 2918764df..ed5e6fbe2 100644
--- a/web/app.js
+++ b/web/app.js
@@ -17,7 +17,7 @@
 import {
   animationStarted, DEFAULT_SCALE_VALUE, getPDFFileNameFromURL, isValidRotation,
   MAX_SCALE, MIN_SCALE, noContextMenuHandler, normalizeWheelEventDelta,
-  parseQueryString, ProgressBar, RendererType
+  parseQueryString, PresentationModeState, ProgressBar, RendererType
 } from './ui_utils';
 import {
   build, createBlob, getDocument, getFilenameFromUrl, InvalidPDFException,
@@ -27,7 +27,6 @@ import {
 import { CursorTool, PDFCursorTools } from './pdf_cursor_tools';
 import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue';
 import { PDFSidebar, SidebarView } from './pdf_sidebar';
-import { PDFViewer, PresentationModeState } from './base_viewer';
 import { getGlobalEventBus } from './dom_events';
 import { OverlayManager } from './overlay_manager';
 import { PasswordPrompt } from './password_prompt';
@@ -40,6 +39,7 @@ import { PDFLinkService } from './pdf_link_service';
 import { PDFOutlineViewer } from './pdf_outline_viewer';
 import { PDFPresentationMode } from './pdf_presentation_mode';
 import { PDFThumbnailViewer } from './pdf_thumbnail_viewer';
+import { PDFViewer } from './pdf_viewer';
 import { SecondaryToolbar } from './secondary_toolbar';
 import { Toolbar } from './toolbar';
 import { ViewHistory } from './view_history';
diff --git a/web/base_viewer.js b/web/base_viewer.js
index b4f9e3b38..dcb7125de 100644
--- a/web/base_viewer.js
+++ b/web/base_viewer.js
@@ -15,9 +15,9 @@
 
 import { createPromiseCapability, PDFJS } from 'pdfjs-lib';
 import {
-  CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, getVisibleElements,
-  isValidRotation, MAX_AUTO_SCALE, NullL10n, RendererType, SCROLLBAR_PADDING,
-  scrollIntoView, UNKNOWN_SCALE, VERTICAL_PADDING, watchScroll
+  CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, isValidRotation,
+  MAX_AUTO_SCALE, NullL10n, PresentationModeState, RendererType,
+  SCROLLBAR_PADDING, UNKNOWN_SCALE, VERTICAL_PADDING, watchScroll
 } from './ui_utils';
 import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue';
 import { AnnotationLayerBuilder } from './annotation_layer_builder';
@@ -26,13 +26,6 @@ import { PDFPageView } from './pdf_page_view';
 import { SimpleLinkService } from './pdf_link_service';
 import { TextLayerBuilder } from './text_layer_builder';
 
-const PresentationModeState = {
-  UNKNOWN: 0,
-  NORMAL: 1,
-  CHANGING: 2,
-  FULLSCREEN: 3,
-};
-
 const DEFAULT_CACHE_SIZE = 10;
 
 /**
@@ -60,7 +53,7 @@ const DEFAULT_CACHE_SIZE = 10;
 
 function PDFPageViewBuffer(size) {
   let data = [];
-  this.push = function cachePush(view) {
+  this.push = function(view) {
     let i = data.indexOf(view);
     if (i >= 0) {
       data.splice(i, 1);
@@ -70,7 +63,7 @@ function PDFPageViewBuffer(size) {
       data.shift().destroy();
     }
   };
-  this.resize = function (newSize) {
+  this.resize = function(newSize) {
     size = newSize;
     while (data.length > size) {
       data.shift().destroy();
@@ -98,11 +91,16 @@ function isPortraitOrientation(size) {
  * Simple viewer control to display PDF content/pages.
  * @implements {IRenderableView}
  */
-class PDFViewer {
+class BaseViewer {
   /**
    * @param {PDFViewerOptions} options
    */
   constructor(options) {
+    if (this.constructor === BaseViewer) {
+      throw new Error('Cannot initialize BaseViewer.');
+    }
+    this._name = this.constructor.name;
+
     this.container = options.container;
     this.viewer = options.viewer || options.container.firstElementChild;
     this.eventBus = options.eventBus || getGlobalEventBus();
@@ -182,7 +180,7 @@ class PDFViewer {
 
     if (!(0 < val && val <= this.pagesCount)) {
       console.error(
-        `PDFViewer._setCurrentPageNumber: "${val}" is out of bounds.`);
+        `${this._name}._setCurrentPageNumber: "${val}" is out of bounds.`);
       return;
     }
 
@@ -305,6 +303,10 @@ class PDFViewer {
     }
   }
 
+  get _setDocumentViewerElement() {
+    throw new Error('Not implemented: _setDocumentViewerElement');
+  }
+
   /**
    * @param pdfDocument {PDFDocument}
    */
@@ -364,7 +366,7 @@ class PDFViewer {
           textLayerFactory = this;
         }
         let pageView = new PDFPageView({
-          container: this.viewer,
+          container: this._setDocumentViewerElement,
           eventBus: this.eventBus,
           id: pageNum,
           scale,
@@ -437,7 +439,7 @@ class PDFViewer {
     } else if (!(labels instanceof Array &&
                  this.pdfDocument.numPages === labels.length)) {
       this._pageLabels = null;
-      console.error('PDFViewer.setPageLabels: Invalid page labels.');
+      console.error(`${this._name}.setPageLabels: Invalid page labels.`);
     } else {
       this._pageLabels = labels;
     }
@@ -472,6 +474,10 @@ class PDFViewer {
     this.update();
   }
 
+  _scrollIntoView({ pageDiv, pageSpot = null, pageNumber = null, }) {
+    throw new Error('Not implemented: _scrollIntoView');
+  }
+
   _setScaleDispatchEvent(newScale, newValue, preset = false) {
     let arg = {
       source: this,
@@ -560,7 +566,7 @@ class PDFViewer {
           break;
         default:
           console.error(
-            `PDFViewer._setScale: "${value}" is an unknown zoom value.`);
+            `${this._name}._setScale: "${value}" is an unknown zoom value.`);
           return;
       }
       this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ true);
@@ -578,7 +584,7 @@ class PDFViewer {
     }
 
     let pageView = this._pages[this._currentPageNumber - 1];
-    scrollIntoView(pageView.div);
+    this._scrollIntoView({ pageDiv: pageView.div, });
   }
 
   /**
@@ -615,7 +621,7 @@ class PDFViewer {
     let pageView = this._pages[pageNumber - 1];
     if (!pageView) {
       console.error(
-        'PDFViewer.scrollPageIntoView: Invalid "pageNumber" parameter.');
+        `${this._name}.scrollPageIntoView: Invalid "pageNumber" parameter.`);
       return;
     }
     let x = 0, y = 0;
@@ -675,7 +681,7 @@ class PDFViewer {
         scale = Math.min(Math.abs(widthScale), Math.abs(heightScale));
         break;
       default:
-        console.error(`PDFViewer.scrollPageIntoView: "${dest[1].name}" ` +
+        console.error(`${this._name}.scrollPageIntoView: "${dest[1].name}" ` +
                       'is not a valid destination type.');
         return;
     }
@@ -687,7 +693,10 @@ class PDFViewer {
     }
 
     if (scale === 'page-fit' && !dest[4]) {
-      scrollIntoView(pageView.div);
+      this._scrollIntoView({
+        pageDiv: pageView.div,
+        pageNumber,
+      });
       return;
     }
 
@@ -705,7 +714,17 @@ class PDFViewer {
       left = Math.max(left, 0);
       top = Math.max(top, 0);
     }
-    scrollIntoView(pageView.div, { left, top, });
+    this._scrollIntoView({
+      pageDiv: pageView.div,
+      pageSpot: { left, top, },
+      pageNumber,
+    });
+  }
+
+  _resizeBuffer(numVisiblePages) {
+    let suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE,
+                                      2 * numVisiblePages + 1);
+    this._buffer.resize(suggestedCacheSize);
   }
 
   _updateLocation(firstPage) {
@@ -738,48 +757,7 @@ class PDFViewer {
   }
 
   update() {
-    let visible = this._getVisiblePages();
-    let visiblePages = visible.views;
-    if (visiblePages.length === 0) {
-      return;
-    }
-
-    let suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE,
-                                      2 * visiblePages.length + 1);
-    this._buffer.resize(suggestedCacheSize);
-
-    this.renderingQueue.renderHighestPriority(visible);
-
-    let currentId = this._currentPageNumber;
-    let firstPage = visible.first;
-    let stillFullyVisible = false;
-
-    for (let i = 0, ii = visiblePages.length; i < ii; ++i) {
-      let page = visiblePages[i];
-
-      if (page.percent < 100) {
-        break;
-      }
-      if (page.id === currentId) {
-        stillFullyVisible = true;
-        break;
-      }
-    }
-
-    if (!stillFullyVisible) {
-      currentId = visiblePages[0].id;
-    }
-
-    if (!this.isInPresentationMode) {
-      this._setCurrentPageNumber(currentId);
-    }
-
-    this._updateLocation(firstPage);
-
-    this.eventBus.dispatch('updateviewarea', {
-      source: this,
-      location: this._location,
-    });
+    throw new Error('Not implemented: update');
   }
 
   containsElement(element) {
@@ -804,15 +782,7 @@ class PDFViewer {
   }
 
   _getVisiblePages() {
-    if (!this.isInPresentationMode) {
-      return getVisibleElements(this.container, this._pages, true);
-    }
-    // The algorithm in getVisibleElements doesn't work in all browsers and
-    // configurations when presentation mode is active.
-    let visible = [];
-    let currentPage = this._pages[this._currentPageNumber - 1];
-    visible.push({ id: currentPage.id, view: currentPage, });
-    return { first: currentPage, last: currentPage, views: visible, };
+    throw new Error('Not implemented: _getVisiblePages');
   }
 
   cleanup() {
@@ -974,6 +944,5 @@ class PDFViewer {
 }
 
 export {
-  PresentationModeState,
-  PDFViewer,
+  BaseViewer,
 };
diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js
index 42b7cba2a..584aa9ed6 100644
--- a/web/pdf_viewer.component.js
+++ b/web/pdf_viewer.component.js
@@ -16,7 +16,7 @@
 'use strict';
 
 var pdfjsLib = require('./pdfjs.js');
-var pdfjsWebPDFViewer = require('./base_viewer.js');
+var pdfjsWebPDFViewer = require('./pdf_viewer.js');
 var pdfjsWebPDFPageView = require('./pdf_page_view.js');
 var pdfjsWebPDFLinkService = require('./pdf_link_service.js');
 var pdfjsWebTextLayerBuilder = require('./text_layer_builder.js');
diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js
new file mode 100644
index 000000000..c32065ff6
--- /dev/null
+++ b/web/pdf_viewer.js
@@ -0,0 +1,83 @@
+/* Copyright 2014 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 { getVisibleElements, scrollIntoView } from './ui_utils';
+import { BaseViewer } from './base_viewer';
+import { shadow } from 'pdfjs-lib';
+
+class PDFViewer extends BaseViewer {
+  get _setDocumentViewerElement() {
+    return shadow(this, '_setDocumentViewerElement', this.viewer);
+  }
+
+  _scrollIntoView({ pageDiv, pageSpot = null, }) {
+    scrollIntoView(pageDiv, pageSpot);
+  }
+
+  _getVisiblePages() {
+    if (!this.isInPresentationMode) {
+      return getVisibleElements(this.container, this._pages, true);
+    }
+    // The algorithm in getVisibleElements doesn't work in all browsers and
+    // configurations when presentation mode is active.
+    let currentPage = this._pages[this._currentPageNumber - 1];
+    let visible = [{ id: currentPage.id, view: currentPage, }];
+    return { first: currentPage, last: currentPage, views: visible, };
+  }
+
+  update() {
+    let visible = this._getVisiblePages();
+    let visiblePages = visible.views, numVisiblePages = visiblePages.length;
+
+    if (numVisiblePages === 0) {
+      return;
+    }
+    this._resizeBuffer(numVisiblePages);
+
+    this.renderingQueue.renderHighestPriority(visible);
+
+    let currentId = this._currentPageNumber;
+    let stillFullyVisible = false;
+
+    for (let i = 0; i < numVisiblePages; ++i) {
+      let page = visiblePages[i];
+
+      if (page.percent < 100) {
+        break;
+      }
+      if (page.id === currentId) {
+        stillFullyVisible = true;
+        break;
+      }
+    }
+
+    if (!stillFullyVisible) {
+      currentId = visiblePages[0].id;
+    }
+    if (!this.isInPresentationMode) {
+      this._setCurrentPageNumber(currentId);
+    }
+
+    this._updateLocation(visible.first);
+    this.eventBus.dispatch('updateviewarea', {
+      source: this,
+      location: this._location,
+    });
+  }
+}
+
+export {
+  PDFViewer,
+};
diff --git a/web/ui_utils.js b/web/ui_utils.js
index aec8a5793..394e1e625 100644
--- a/web/ui_utils.js
+++ b/web/ui_utils.js
@@ -25,6 +25,13 @@ const MAX_AUTO_SCALE = 1.25;
 const SCROLLBAR_PADDING = 40;
 const VERTICAL_PADDING = 5;
 
+const PresentationModeState = {
+  UNKNOWN: 0,
+  NORMAL: 1,
+  CHANGING: 2,
+  FULLSCREEN: 3,
+};
+
 const RendererType = {
   CANVAS: 'canvas',
   SVG: 'svg',
@@ -661,6 +668,7 @@ export {
   VERTICAL_PADDING,
   isValidRotation,
   cloneObj,
+  PresentationModeState,
   RendererType,
   mozL10n,
   NullL10n,