From a4402c84de8b1f5c87dbb86e4506e5d74674e272 Mon Sep 17 00:00:00 2001
From: Yury Delendik <ydelendik@mozilla.com>
Date: Fri, 18 Nov 2016 12:50:29 -0600
Subject: [PATCH] Refactor toolbar (and secondary toolbar).

---
 extensions/firefox/tools/l10n.js |   5 +
 external/webL10n/l10n.js         |   9 +-
 web/app.js                       | 317 +++++++++----------------------
 web/secondary_toolbar.js         |  33 ++++
 web/toolbar.js                   | 290 ++++++++++++++++++++++++++++
 web/ui_utils.js                  |  32 +++-
 web/viewer.js                    |   2 -
 7 files changed, 457 insertions(+), 231 deletions(-)
 create mode 100644 web/toolbar.js

diff --git a/extensions/firefox/tools/l10n.js b/extensions/firefox/tools/l10n.js
index 2e0684e06..04346ee1d 100644
--- a/extensions/firefox/tools/l10n.js
+++ b/extensions/firefox/tools/l10n.js
@@ -5,6 +5,7 @@
 (function(window) {
   var gLanguage = '';
   var gExternalLocalizerServices = null;
+  var gReadyState = 'loading';
 
   // fetch an l10n objects
   function getL10nData(key) {
@@ -99,6 +100,8 @@
 
     translateFragment();
 
+    gReadyState = 'complete';
+
     // fire a 'localized' DOM event
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('localized', false, false);
@@ -135,6 +138,8 @@
       return (rtlList.indexOf(shortCode) >= 0) ? 'rtl' : 'ltr';
     },
 
+    getReadyState: function() { return gReadyState; },
+
     setExternalLocalizerServices: function (externalLocalizerServices) {
       gExternalLocalizerServices = externalLocalizerServices;
 
diff --git a/external/webL10n/l10n.js b/external/webL10n/l10n.js
index 3d5ecffaf..2190c5385 100644
--- a/external/webL10n/l10n.js
+++ b/external/webL10n/l10n.js
@@ -101,9 +101,7 @@ document.webL10n = (function(window, document, undefined) {
 
   function xhrLoadText(url, onSuccess, onFailure) {
     onSuccess = onSuccess || function _onSuccess(data) {};
-    onFailure = onFailure || function _onFailure() {
-      console.warn(url + ' not found.');
-    };
+    onFailure = onFailure || function _onFailure() {};
 
     var xhr = new XMLHttpRequest();
     xhr.open('GET', url, gAsyncResourceLoading);
@@ -244,7 +242,10 @@ document.webL10n = (function(window, document, undefined) {
       function loadImport(url, callback) {
         xhrLoadText(url, function(content) {
           parseRawLines(content, false, callback); // don't allow recursive imports
-        }, null);
+        }, function () {
+          console.warn(url + ' not found.');
+          callback();
+        });
       }
 
       // fill the dictionary
diff --git a/web/app.js b/web/app.js
index 69f268fe1..b53d779c5 100644
--- a/web/app.js
+++ b/web/app.js
@@ -22,20 +22,22 @@
       'pdfjs-web/download_manager', 'pdfjs-web/pdf_history',
       'pdfjs-web/preferences', 'pdfjs-web/pdf_sidebar',
       'pdfjs-web/view_history', 'pdfjs-web/pdf_thumbnail_viewer',
-      'pdfjs-web/secondary_toolbar', 'pdfjs-web/password_prompt',
-      'pdfjs-web/pdf_presentation_mode', 'pdfjs-web/pdf_document_properties',
-      'pdfjs-web/hand_tool', 'pdfjs-web/pdf_viewer',
-      'pdfjs-web/pdf_rendering_queue', 'pdfjs-web/pdf_link_service',
-      'pdfjs-web/pdf_outline_viewer', 'pdfjs-web/overlay_manager',
-      'pdfjs-web/pdf_attachment_viewer', 'pdfjs-web/pdf_find_controller',
-      'pdfjs-web/pdf_find_bar', 'pdfjs-web/dom_events', 'pdfjs-web/pdfjs'],
+      'pdfjs-web/toolbar', 'pdfjs-web/secondary_toolbar',
+      'pdfjs-web/password_prompt', 'pdfjs-web/pdf_presentation_mode',
+      'pdfjs-web/pdf_document_properties', 'pdfjs-web/hand_tool',
+      'pdfjs-web/pdf_viewer', 'pdfjs-web/pdf_rendering_queue',
+      'pdfjs-web/pdf_link_service', 'pdfjs-web/pdf_outline_viewer',
+      'pdfjs-web/overlay_manager', 'pdfjs-web/pdf_attachment_viewer',
+      'pdfjs-web/pdf_find_controller', 'pdfjs-web/pdf_find_bar',
+      'pdfjs-web/dom_events', 'pdfjs-web/pdfjs'],
       factory);
   } else if (typeof exports !== 'undefined') {
     factory(exports, require('./ui_utils.js'), require('./download_manager.js'),
       require('./pdf_history.js'), require('./preferences.js'),
       require('./pdf_sidebar.js'), require('./view_history.js'),
-      require('./pdf_thumbnail_viewer.js'), require('./secondary_toolbar.js'),
-      require('./password_prompt.js'), require('./pdf_presentation_mode.js'),
+      require('./pdf_thumbnail_viewer.js'), require('./toolbar.js'),
+      require('./secondary_toolbar.js'), require('./password_prompt.js'),
+      require('./pdf_presentation_mode.js'),
       require('./pdf_document_properties.js'), require('./hand_tool.js'),
       require('./pdf_viewer.js'), require('./pdf_rendering_queue.js'),
       require('./pdf_link_service.js'), require('./pdf_outline_viewer.js'),
@@ -47,25 +49,28 @@
       root.pdfjsWebDownloadManager, root.pdfjsWebPDFHistory,
       root.pdfjsWebPreferences, root.pdfjsWebPDFSidebar,
       root.pdfjsWebViewHistory, root.pdfjsWebPDFThumbnailViewer,
-      root.pdfjsWebSecondaryToolbar, root.pdfjsWebPasswordPrompt,
-      root.pdfjsWebPDFPresentationMode, root.pdfjsWebPDFDocumentProperties,
-      root.pdfjsWebHandTool, root.pdfjsWebPDFViewer,
-      root.pdfjsWebPDFRenderingQueue, root.pdfjsWebPDFLinkService,
-      root.pdfjsWebPDFOutlineViewer, root.pdfjsWebOverlayManager,
-      root.pdfjsWebPDFAttachmentViewer, root.pdfjsWebPDFFindController,
-      root.pdfjsWebPDFFindBar, root.pdfjsWebDOMEvents, root.pdfjsWebPDFJS);
+      root.pdfjsWebToolbar, root.pdfjsWebSecondaryToolbar,
+      root.pdfjsWebPasswordPrompt, root.pdfjsWebPDFPresentationMode,
+      root.pdfjsWebPDFDocumentProperties, root.pdfjsWebHandTool,
+      root.pdfjsWebPDFViewer, root.pdfjsWebPDFRenderingQueue,
+      root.pdfjsWebPDFLinkService, root.pdfjsWebPDFOutlineViewer,
+      root.pdfjsWebOverlayManager, root.pdfjsWebPDFAttachmentViewer,
+      root.pdfjsWebPDFFindController, root.pdfjsWebPDFFindBar,
+      root.pdfjsWebDOMEvents, root.pdfjsWebPDFJS);
   }
 }(this, function (exports, uiUtilsLib, downloadManagerLib, pdfHistoryLib,
                   preferencesLib, pdfSidebarLib, viewHistoryLib,
-                  pdfThumbnailViewerLib, secondaryToolbarLib, passwordPromptLib,
-                  pdfPresentationModeLib, pdfDocumentPropertiesLib, handToolLib,
-                  pdfViewerLib, pdfRenderingQueueLib, pdfLinkServiceLib,
-                  pdfOutlineViewerLib, overlayManagerLib,
-                  pdfAttachmentViewerLib, pdfFindControllerLib, pdfFindBarLib,
-                  domEventsLib, pdfjsLib) {
+                  pdfThumbnailViewerLib, toolbarLib, secondaryToolbarLib,
+                  passwordPromptLib, pdfPresentationModeLib,
+                  pdfDocumentPropertiesLib, handToolLib, pdfViewerLib,
+                  pdfRenderingQueueLib, pdfLinkServiceLib, pdfOutlineViewerLib,
+                  overlayManagerLib, pdfAttachmentViewerLib,
+                  pdfFindControllerLib, pdfFindBarLib, domEventsLib, pdfjsLib) {
 
 var UNKNOWN_SCALE = uiUtilsLib.UNKNOWN_SCALE;
 var DEFAULT_SCALE_VALUE = uiUtilsLib.DEFAULT_SCALE_VALUE;
+var MIN_SCALE = uiUtilsLib.MIN_SCALE;
+var MAX_SCALE = uiUtilsLib.MAX_SCALE;
 var ProgressBar = uiUtilsLib.ProgressBar;
 var getPDFFileNameFromURL = uiUtilsLib.getPDFFileNameFromURL;
 var noContextMenuHandler = uiUtilsLib.noContextMenuHandler;
@@ -77,6 +82,7 @@ var SidebarView = pdfSidebarLib.SidebarView;
 var PDFSidebar = pdfSidebarLib.PDFSidebar;
 var ViewHistory = viewHistoryLib.ViewHistory;
 var PDFThumbnailViewer = pdfThumbnailViewerLib.PDFThumbnailViewer;
+var Toolbar = toolbarLib.Toolbar;
 var SecondaryToolbar = secondaryToolbarLib.SecondaryToolbar;
 var PasswordPrompt = passwordPromptLib.PasswordPrompt;
 var PDFPresentationMode = pdfPresentationModeLib.PDFPresentationMode;
@@ -94,13 +100,10 @@ var PDFFindController = pdfFindControllerLib.PDFFindController;
 var PDFFindBar = pdfFindBarLib.PDFFindBar;
 var getGlobalEventBus = domEventsLib.getGlobalEventBus;
 var normalizeWheelEventDelta = uiUtilsLib.normalizeWheelEventDelta;
+var animationStarted = uiUtilsLib.animationStarted;
+var localized = uiUtilsLib.localized;
 
 var DEFAULT_SCALE_DELTA = 1.1;
-var MIN_SCALE = 0.25;
-var MAX_SCALE = 10.0;
-var SCALE_SELECT_CONTAINER_PADDING = 8;
-var SCALE_SELECT_PADDING = 22;
-var PAGE_NUMBER_LOADING_INDICATOR = 'visiblePageIsLoading';
 var DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT = 5000;
 
 function configure(PDFJS) {
@@ -169,11 +172,14 @@ var PDFViewerApplication = {
   store: null,
   /** @type {DownloadManager} */
   downloadManager: null,
+  /** @type {Toolbar} */
+  toolbar: null,
+  /** @type {SecondaryToolbar} */
+  secondaryToolbar: null,
   /** @type {EventBus} */
   eventBus: null,
   pageRotation: 0,
   isInitialViewSet: false,
-  animationStartedPromise: null,
   preferenceSidebarViewOnLoad: SidebarView.NONE,
   preferencePdfBugEnabled: false,
   preferenceShowPreviousViewOnLoad: true,
@@ -183,7 +189,6 @@ var PDFViewerApplication = {
   url: '',
   baseUrl: '',
   externalServices: DefaultExernalServices,
-  hasPageLabels: false,
 
   // called once when the document is loaded
   initialize: function pdfViewInitialize(appConfig) {
@@ -274,6 +279,8 @@ var PDFViewerApplication = {
     this.pdfDocumentProperties =
       new PDFDocumentProperties(appConfig.documentProperties);
 
+    this.toolbar = new Toolbar(appConfig.toolbar, container, eventBus);
+
     this.secondaryToolbar =
       new SecondaryToolbar(appConfig.secondaryToolbar, container, eventBus);
 
@@ -572,7 +579,6 @@ var PDFViewerApplication = {
     }
     this.store = null;
     this.isInitialViewSet = false;
-    this.hasPageLabels = false;
 
     this.pdfSidebar.reset();
     this.pdfOutlineViewer.reset();
@@ -580,6 +586,8 @@ var PDFViewerApplication = {
 
     this.findController.reset();
     this.findBar.reset();
+    this.toolbar.reset();
+    this.secondaryToolbar.reset();
 
     if (typeof PDFBug !== 'undefined') {
       PDFBug.cleanup();
@@ -859,9 +867,8 @@ var PDFViewerApplication = {
       self.loadingBar.hide();
     });
 
-    this._updateUIToolbar({
-      resetNumPages: true,
-    });
+    this.toolbar.setPagesCount(pdfDocument.numPages, false);
+    this.secondaryToolbar.setPagesCount(pdfDocument.numPages);
 
     var id = this.documentFingerprint = pdfDocument.fingerprint;
     var store = this.store = new ViewHistory(id);
@@ -987,10 +994,11 @@ var PDFViewerApplication = {
       pdfViewer.setPageLabels(labels);
       pdfThumbnailViewer.setPageLabels(labels);
 
-      self.hasPageLabels = true;
-      self._updateUIToolbar({
-        resetNumPages: true,
-      });
+      // Changing toolbar page display to use labels and we need to set
+      // the label of the current page.
+      self.toolbar.setPagesCount(pdfDocument.numPages, true);
+      self.toolbar.setPageNumber(pdfViewer.currentPageNumber,
+                                 pdfViewer.currentPageLabel);
     });
 
     pagesPromise.then(function() {
@@ -1015,8 +1023,7 @@ var PDFViewerApplication = {
       }
     });
 
-    Promise.all([onePageRendered, this.animationStartedPromise]).then(
-        function() {
+    Promise.all([onePageRendered, animationStarted]).then(function() {
       pdfDocument.getOutline().then(function(outline) {
         self.pdfOutlineViewer.render({ outline: outline });
       });
@@ -1114,6 +1121,12 @@ var PDFViewerApplication = {
       this.page = 1;
     }
 
+    // Ensure that the correct page number is displayed in the UI,
+    // even if the active page didn't change during document load.
+    this.toolbar.setPageNumber(this.pdfViewer.currentPageNumber,
+                               this.pdfViewer.currentPageLabel);
+    this.secondaryToolbar.setPageNumber(this.pdfViewer.currentPageNumber);
+
     if (!this.pdfViewer.currentScaleValue) {
       // Scale was not initialized: invalid bookmark or scale was not specified.
       // Setting the default one.
@@ -1217,87 +1230,10 @@ var PDFViewerApplication = {
     this.pdfPresentationMode.request();
   },
 
-  /**
-   * @typedef UpdateUIToolbarParameters
-   * @property {number} pageNumber
-   * @property {string} pageLabel
-   * @property {string} scaleValue
-   * @property {number} scale
-   * @property {boolean} resetNumPages
-   */
-
-  /**
-   * @param {Object} UpdateUIToolbarParameters
-   * @private
-   */
-  _updateUIToolbar: function (params) {
-    function selectScaleOption(value, scale) {
-      var options = toolbarConfig.scaleSelect.options;
-      var predefinedValueFound = false;
-      for (var i = 0, ii = options.length; i < ii; i++) {
-        var option = options[i];
-        if (option.value !== value) {
-          option.selected = false;
-          continue;
-        }
-        option.selected = true;
-        predefinedValueFound = true;
-      }
-      if (!predefinedValueFound) {
-        var customScale = Math.round(scale * 10000) / 100;
-        toolbarConfig.customScaleOption.textContent =
-          mozL10n.get('page_scale_percent', {scale: customScale}, '{{scale}}%');
-        toolbarConfig.customScaleOption.selected = true;
-      }
-    }
-
-    var pageNumber = params.pageNumber || this.pdfViewer.currentPageNumber;
-    var scaleValue = (params.scaleValue || params.scale ||
-      this.pdfViewer.currentScaleValue || DEFAULT_SCALE_VALUE).toString();
-    var scale = params.scale || this.pdfViewer.currentScale;
-    var resetNumPages = params.resetNumPages || false;
-
-    var toolbarConfig = this.appConfig.toolbar;
-    var pagesCount = this.pagesCount;
-
-    if (resetNumPages) {
-      if (this.hasPageLabels) {
-        toolbarConfig.pageNumber.type = 'text';
-      } else {
-        toolbarConfig.pageNumber.type = 'number';
-        toolbarConfig.numPages.textContent = mozL10n.get('of_pages',
-          { pagesCount: pagesCount }, 'of {{pagesCount}}');
-      }
-      toolbarConfig.pageNumber.max = pagesCount;
-    }
-
-    if (this.hasPageLabels) {
-      toolbarConfig.pageNumber.value = params.pageLabel ||
-                                       this.pdfViewer.currentPageLabel;
-      toolbarConfig.numPages.textContent = mozL10n.get('page_of_pages',
-        { pageNumber: pageNumber, pagesCount: pagesCount },
-        '({{pageNumber}} of {{pagesCount}})');
-    } else {
-      toolbarConfig.pageNumber.value = pageNumber;
-    }
-
-    toolbarConfig.previous.disabled = (pageNumber <= 1);
-    toolbarConfig.next.disabled = (pageNumber >= pagesCount);
-
-    toolbarConfig.firstPage.disabled = (pageNumber <= 1);
-    toolbarConfig.lastPage.disabled = (pageNumber >= pagesCount);
-
-    toolbarConfig.zoomOut.disabled = (scale <= MIN_SCALE);
-    toolbarConfig.zoomIn.disabled = (scale >= MAX_SCALE);
-
-    selectScaleOption(scaleValue, scale);
-  },
-
   bindEvents: function pdfViewBindEvents() {
     var eventBus = this.eventBus;
 
     eventBus.on('resize', webViewerResize);
-    eventBus.on('localized', webViewerLocalized);
     eventBus.on('hashchange', webViewerHashchange);
     eventBus.on('beforeprint', this.beforePrint.bind(this));
     eventBus.on('afterprint', this.afterPrint.bind(this));
@@ -1316,6 +1252,12 @@ var PDFViewerApplication = {
     eventBus.on('download', webViewerDownload);
     eventBus.on('firstpage', webViewerFirstPage);
     eventBus.on('lastpage', webViewerLastPage);
+    eventBus.on('nextpage', webViewerNextPage);
+    eventBus.on('previouspage', webViewerPreviousPage);
+    eventBus.on('zoomin', webViewerZoomIn);
+    eventBus.on('zoomout', webViewerZoomOut);
+    eventBus.on('pagenumberchanged', webViewerPageNumberChanged);
+    eventBus.on('scalechanged', webViewerScaleChanged);
     eventBus.on('rotatecw', webViewerRotateCw);
     eventBus.on('rotateccw', webViewerRotateCcw);
     eventBus.on('documentproperties', webViewerDocumentProperties);
@@ -1509,9 +1451,6 @@ function webViewerInitialized() {
     appConfig.toolbar.viewFind.classList.add('hidden');
   }
 
-  // Suppress context menus for some controls
-  appConfig.toolbar.scaleSelect.oncontextmenu = noContextMenuHandler;
-
   appConfig.sidebar.mainContainer.addEventListener('transitionend',
     function(e) {
       if (e.target === /* mainContainer */ this) {
@@ -1523,63 +1462,6 @@ function webViewerInitialized() {
     PDFViewerApplication.pdfSidebar.toggle();
   });
 
-  appConfig.toolbar.previous.addEventListener('click', function() {
-    PDFViewerApplication.page--;
-  });
-
-  appConfig.toolbar.next.addEventListener('click', function() {
-    PDFViewerApplication.page++;
-  });
-
-  appConfig.toolbar.zoomIn.addEventListener('click', function() {
-    PDFViewerApplication.zoomIn();
-  });
-
-  appConfig.toolbar.zoomOut.addEventListener('click', function() {
-    PDFViewerApplication.zoomOut();
-  });
-
-  appConfig.toolbar.pageNumber.addEventListener('click', function() {
-    this.select();
-  });
-
-  appConfig.toolbar.pageNumber.addEventListener('change', function() {
-    var pdfViewer = PDFViewerApplication.pdfViewer;
-    pdfViewer.currentPageLabel = this.value;
-
-    // Ensure that the page number input displays the correct value, even if the
-    // value entered by the user was invalid (e.g. a floating point number).
-    if (this.value !== pdfViewer.currentPageNumber.toString() &&
-        this.value !== pdfViewer.currentPageLabel) {
-      PDFViewerApplication._updateUIToolbar({});
-    }
-  });
-
-  appConfig.toolbar.scaleSelect.addEventListener('change', function() {
-    if (this.value === 'custom') {
-      return;
-    }
-    PDFViewerApplication.pdfViewer.currentScaleValue = this.value;
-  });
-
-  appConfig.toolbar.presentationModeButton.addEventListener('click',
-      function (e) {
-    PDFViewerApplication.eventBus.dispatch('presentationmode');
-
-  });
-
-  appConfig.toolbar.openFile.addEventListener('click', function (e) {
-    PDFViewerApplication.eventBus.dispatch('openfile');
-  });
-
-  appConfig.toolbar.print.addEventListener('click', function (e) {
-    PDFViewerApplication.eventBus.dispatch('print');
-  });
-
-  appConfig.toolbar.download.addEventListener('click', function (e) {
-    PDFViewerApplication.eventBus.dispatch('download');
-  });
-
   Promise.all(waitForBeforeOpening).then(function () {
     webViewerOpenFileViaURL(file);
   }).catch(function (reason) {
@@ -1636,8 +1518,7 @@ function webViewerPageRendered(e) {
   // If the page is still visible when it has finished rendering,
   // ensure that the page number input loading indicator is hidden.
   if (pageNumber === PDFViewerApplication.page) {
-    var pageNumberInput = PDFViewerApplication.appConfig.toolbar.pageNumber;
-    pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR);
+    PDFViewerApplication.toolbar.updateLoadingIndicatorState(false);
   }
 
   // Prevent errors in the edge-case where the PDF document is removed *before*
@@ -1789,15 +1670,10 @@ function webViewerUpdateViewarea(e) {
                                                         location.pageNumber);
 
   // Show/hide the loading indicator in the page number input element.
-  var pageNumberInput = PDFViewerApplication.appConfig.toolbar.pageNumber;
   var currentPage =
     PDFViewerApplication.pdfViewer.getPageView(PDFViewerApplication.page - 1);
-
-  if (currentPage.renderingState === RenderingStates.FINISHED) {
-    pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR);
-  } else {
-    pageNumberInput.classList.add(PAGE_NUMBER_LOADING_INDICATOR);
-  }
+  var loading = currentPage.renderingState !== RenderingStates.FINISHED;
+  PDFViewerApplication.toolbar.updateLoadingIndicatorState(loading);
 }
 
 window.addEventListener('resize', function webViewerResize(evt) {
@@ -1884,31 +1760,9 @@ if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) {
   };
 }
 
-window.addEventListener('localized', function localized(evt) {
-  PDFViewerApplication.eventBus.dispatch('localized');
-});
-
 function webViewerLocalized() {
   document.getElementsByTagName('html')[0].dir = mozL10n.getDirection();
-
-  PDFViewerApplication.animationStartedPromise.then(function() {
-    // Adjust the width of the zoom box to fit the content.
-    // Note: If the window is narrow enough that the zoom box is not visible,
-    //       we temporarily show it to be able to adjust its width.
-    var container = PDFViewerApplication.appConfig.toolbar.scaleSelectContainer;
-    if (container.clientWidth === 0) {
-      container.setAttribute('style', 'display: inherit;');
-    }
-    if (container.clientWidth > 0) {
-      var select = PDFViewerApplication.appConfig.toolbar.scaleSelect;
-      select.setAttribute('style', 'min-width: inherit;');
-      var width = select.clientWidth + SCALE_SELECT_CONTAINER_PADDING;
-      select.setAttribute('style', 'min-width: ' +
-                                   (width + SCALE_SELECT_PADDING) + 'px;');
-      container.setAttribute('style', 'min-width: ' + width + 'px; ' +
-                                      'max-width: ' + width + 'px;');
-    }
-  });
+  PDFViewerApplication.eventBus.dispatch('localized');
 }
 
 function webViewerPresentationMode() {
@@ -1934,6 +1788,33 @@ function webViewerLastPage() {
     PDFViewerApplication.page = PDFViewerApplication.pagesCount;
   }
 }
+function webViewerNextPage() {
+  PDFViewerApplication.page++;
+}
+function webViewerPreviousPage() {
+  PDFViewerApplication.page--;
+}
+function webViewerZoomIn() {
+  PDFViewerApplication.zoomIn();
+}
+function webViewerZoomOut() {
+  PDFViewerApplication.zoomOut();
+}
+function webViewerPageNumberChanged(e) {
+  var pdfViewer = PDFViewerApplication.pdfViewer;
+  pdfViewer.currentPageLabel = e.value;
+
+  // Ensure that the page number input displays the correct value, even if the
+  // value entered by the user was invalid (e.g. a floating point number).
+  if (e.value !== pdfViewer.currentPageNumber.toString() &&
+      e.value !== pdfViewer.currentPageLabel) {
+    PDFViewerApplication.toolbar.setPageNumber(
+      pdfViewer.currentPageNumber, pdfViewer.currentPageLabel);
+  }
+}
+function webViewerScaleChanged(e) {
+  PDFViewerApplication.pdfViewer.currentScaleValue = e.value;
+}
 function webViewerRotateCw() {
   PDFViewerApplication.rotatePages(90);
 }
@@ -1965,10 +1846,7 @@ function webViewerFindFromUrlHash(e) {
 }
 
 function webViewerScaleChanging(e) {
-  PDFViewerApplication._updateUIToolbar({
-    scaleValue: e.presetValue,
-    scale: e.scale,
-  });
+  PDFViewerApplication.toolbar.setPageScale(e.presetValue, e.scale);
 
   if (!PDFViewerApplication.initialized) {
     return;
@@ -1979,10 +1857,8 @@ function webViewerScaleChanging(e) {
 function webViewerPageChanging(e) {
   var page = e.pageNumber;
 
-  PDFViewerApplication._updateUIToolbar({
-    pageNumber: page,
-    pageLabel: e.pageLabel,
-  });
+  PDFViewerApplication.toolbar.setPageNumber(page, e.pageLabel || null);
+  PDFViewerApplication.secondaryToolbar.setPageNumber(page);
 
   if (PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
     PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page);
@@ -2332,14 +2208,7 @@ window.addEventListener('afterprint', function afterPrint(evt) {
   PDFViewerApplication.eventBus.dispatch('afterprint');
 });
 
-(function animationStartedClosure() {
-  // The offsetParent is not set until the pdf.js iframe or object is visible.
-  // Waiting for first animation.
-  PDFViewerApplication.animationStartedPromise = new Promise(
-      function (resolve) {
-    window.requestAnimationFrame(resolve);
-  });
-})();
+localized.then(webViewerLocalized);
 
 /* Abstract factory for the print service. */
 var PDFPrintServiceFactory = {
diff --git a/web/secondary_toolbar.js b/web/secondary_toolbar.js
index d4671bd5d..eb43de586 100644
--- a/web/secondary_toolbar.js
+++ b/web/secondary_toolbar.js
@@ -91,6 +91,12 @@ var SecondaryToolbar = (function SecondaryToolbarClosure() {
       { element: options.documentPropertiesButton,
         eventName: 'documentproperties', close: true }
     ];
+    this.items = {
+      firstPage: options.firstPageButton,
+      lastPage: options.lastPageButton,
+      pageRotateCw: options.pageRotateCwButton,
+      pageRotateCcw: options.pageRotateCcwButton,
+    };
 
     this.mainContainer = mainContainer;
     this.eventBus = eventBus;
@@ -99,6 +105,8 @@ var SecondaryToolbar = (function SecondaryToolbarClosure() {
     this.containerHeight = null;
     this.previousContainerHeight = null;
 
+    this.reset();
+
     // Bind the event listeners for click and hand tool actions.
     this._bindClickListeners();
     this._bindHandToolListener(options.toggleHandToolButton);
@@ -115,6 +123,31 @@ var SecondaryToolbar = (function SecondaryToolbarClosure() {
       return this.opened;
     },
 
+    setPageNumber: function SecondaryToolbar_setPageNumber(pageNumber) {
+      this.pageNumber = pageNumber;
+      this._updateUIState();
+    },
+
+    setPagesCount: function SecondaryToolbar_setPagesCount(pagesCount) {
+      this.pagesCount = pagesCount;
+      this._updateUIState();
+    },
+
+    reset: function SecondaryToolbar_reset() {
+      this.pageNumber = 0;
+      this.pagesCount = 0;
+      this._updateUIState();
+    },
+
+    _updateUIState: function SecondaryToolbar_updateUIState() {
+      var items = this.items;
+
+      items.firstPage.disabled = (this.pageNumber <= 1);
+      items.lastPage.disabled = (this.pageNumber >= this.pagesCount);
+      items.pageRotateCw.disabled = this.pagesCount === 0;
+      items.pageRotateCcw.disabled = this.pagesCount === 0;
+    },
+
     _bindClickListeners: function SecondaryToolbar_bindClickListeners() {
       // Button to toggle the visibility of the secondary toolbar.
       this.toggleButton.addEventListener('click', this.toggle.bind(this));
diff --git a/web/toolbar.js b/web/toolbar.js
new file mode 100644
index 000000000..776858c16
--- /dev/null
+++ b/web/toolbar.js
@@ -0,0 +1,290 @@
+/* Copyright 2016 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.
+ */
+
+'use strict';
+
+(function (root, factory) {
+  if (typeof define === 'function' && define.amd) {
+    define('pdfjs-web/toolbar', ['exports', 'pdfjs-web/ui_utils'],
+      factory);
+  } else if (typeof exports !== 'undefined') {
+    factory(exports, require('./ui_utils.js'));
+  } else {
+    factory((root.pdfjsWebToolbar = {}), root.pdfjsWebUIUtils);
+  }
+}(this, function (exports, uiUtils) {
+
+var mozL10n = uiUtils.mozL10n;
+var noContextMenuHandler = uiUtils.noContextMenuHandler;
+var animationStarted = uiUtils.animationStarted;
+var localized = uiUtils.localized;
+
+var DEFAULT_SCALE_VALUE = uiUtils.DEFAULT_SCALE_VALUE;
+var DEFAULT_SCALE = uiUtils.DEFAULT_SCALE;
+var MIN_SCALE = uiUtils.MIN_SCALE;
+var MAX_SCALE = uiUtils.MAX_SCALE;
+
+var PAGE_NUMBER_LOADING_INDICATOR = 'visiblePageIsLoading';
+var SCALE_SELECT_CONTAINER_PADDING = 8;
+var SCALE_SELECT_PADDING = 22;
+
+/**
+ * @typedef {Object} ToolbarOptions
+ * @property {HTMLDivElement} container - Container for the secondary toolbar.
+ * @property {HTMLSpanElement} numPages - Label that contains number of pages.
+ * @property {HTMLInputElement} pageNumber - Control for display and user input
+ *   of the current page number.
+ * @property {HTMLSpanElement} scaleSelectContainer - Container where scale
+ *   controls are placed. The width is adjusted on UI initialization.
+ * @property {HTMLSelectElement} scaleSelect - Scale selection control.
+ * @property {HTMLOptionElement} customScaleOption - The item used to display
+ *   a non-predefined scale.
+ * @property {HTMLButtonElement} previous - Button to go to the previous page.
+ * @property {HTMLButtonElement} next - Button to go to the next page.
+ * @property {HTMLButtonElement} zoomIn - Button to zoom in the pages.
+ * @property {HTMLButtonElement} zoomOut - Button to zoom out the pages.
+ * @property {HTMLButtonElement} viewFind - Button to open find bar.
+ * @property {HTMLButtonElement} openFile - Button to open a new document.
+ * @property {HTMLButtonElement} presentationModeButton - Button to switch to
+ *   presentation mode.
+ * @property {HTMLButtonElement} download - Button to download the document.
+ * @property {HTMLAElement} viewBookmark - Element to link current url of
+ *   the page view.
+ */
+
+/**
+ * @class
+ */
+var Toolbar = (function ToolbarClosure() {
+  /**
+   * @constructs Toolbar
+   * @param {ToolbarOptions} options
+   * @param {HTMLDivElement} mainContainer
+   * @param {EventBus} eventBus
+   */
+  function Toolbar(options, mainContainer, eventBus) {
+    this.toolbar = options.container;
+    this.mainContainer = mainContainer;
+    this.eventBus = eventBus;
+    this.items = options;
+
+    this._wasLocalized = false;
+    this.reset();
+
+    // Bind the event listeners for click and hand tool actions.
+    this._bindListeners();
+  }
+
+  Toolbar.prototype = {
+    setPageNumber: function (pageNumber, pageLabel) {
+      this.pageNumber = pageNumber;
+      this.pageLabel = pageLabel;
+      this._updateUIState(false);
+    },
+
+    setPagesCount: function (pagesCount, hasPageLabels) {
+      this.pagesCount = pagesCount;
+      this.hasPageLabels = hasPageLabels;
+      this._updateUIState(true);
+    },
+
+    setPageScale: function (pageScaleValue, pageScale) {
+      this.pageScaleValue = pageScaleValue;
+      this.pageScale = pageScale;
+      this._updateUIState(false);
+    },
+
+    reset: function () {
+      this.pageNumber = 0;
+      this.pageLabel = null;
+      this.hasPageLabels = false;
+      this.pagesCount = 0;
+      this.pageScaleValue = DEFAULT_SCALE_VALUE;
+      this.pageScale = DEFAULT_SCALE;
+      this._updateUIState(true);
+    },
+
+    _bindListeners: function Toolbar_bindClickListeners() {
+      var eventBus = this.eventBus;
+      var self = this;
+      var items = this.items;
+
+      items.previous.addEventListener('click', function() {
+        eventBus.dispatch('previouspage');
+      });
+
+      items.next.addEventListener('click', function() {
+        eventBus.dispatch('nextpage');
+      });
+
+      items.zoomIn.addEventListener('click', function() {
+        eventBus.dispatch('zoomin');
+      });
+
+      items.zoomOut.addEventListener('click', function() {
+        eventBus.dispatch('zoomout');
+      });
+
+      items.pageNumber.addEventListener('click', function() {
+        this.select();
+      });
+
+      items.pageNumber.addEventListener('change', function() {
+        eventBus.dispatch('pagenumberchanged', {
+          source: self,
+          value: this.value
+        });
+      });
+
+      items.scaleSelect.addEventListener('change', function() {
+        if (this.value === 'custom') {
+          return;
+        }
+        eventBus.dispatch('scalechanged', {
+          source: self,
+          value: this.value
+        });
+      });
+
+      items.presentationModeButton.addEventListener('click',
+          function (e) {
+        eventBus.dispatch('presentationmode');
+      });
+
+      items.openFile.addEventListener('click', function (e) {
+        eventBus.dispatch('openfile');
+      });
+
+      items.print.addEventListener('click', function (e) {
+        eventBus.dispatch('print');
+      });
+
+      items.download.addEventListener('click', function (e) {
+        eventBus.dispatch('download');
+      });
+
+      // Suppress context menus for some controls
+      items.scaleSelect.oncontextmenu = noContextMenuHandler;
+
+      localized.then(this._localized.bind(this));
+    },
+
+    _localized: function Toolbar_localized() {
+      this._wasLocalized = true;
+      this._adjustScaleWidth();
+      this._updateUIState(true);
+    },
+
+    _updateUIState: function Toolbar_updateUIState(resetNumPages) {
+      function selectScaleOption(value, scale) {
+        var options = items.scaleSelect.options;
+        var predefinedValueFound = false;
+        for (var i = 0, ii = options.length; i < ii; i++) {
+          var option = options[i];
+          if (option.value !== value) {
+            option.selected = false;
+            continue;
+          }
+          option.selected = true;
+          predefinedValueFound = true;
+        }
+        if (!predefinedValueFound) {
+          var customScale = Math.round(scale * 10000) / 100;
+          items.customScaleOption.textContent =
+            mozL10n.get('page_scale_percent', {scale: customScale},
+                        '{{scale}}%');
+          items.customScaleOption.selected = true;
+        }
+      }
+
+      if (!this._wasLocalized) {
+        // Don't update UI state until we will localize the toolbar.
+        return;
+      }
+
+      var pageNumber = this.pageNumber;
+      var scaleValue = (this.pageScaleValue || this.pageScale).toString();
+      var scale = this.pageScale;
+
+      var items = this.items;
+      var pagesCount = this.pagesCount;
+
+      if (resetNumPages) {
+        if (this.hasPageLabels) {
+          items.pageNumber.type = 'text';
+        } else {
+          items.pageNumber.type = 'number';
+          items.numPages.textContent = mozL10n.get('of_pages',
+            { pagesCount: pagesCount }, 'of {{pagesCount}}');
+        }
+        items.pageNumber.max = pagesCount;
+      }
+
+      if (this.hasPageLabels) {
+        items.pageNumber.value = this.pageLabel;
+        items.numPages.textContent = mozL10n.get('page_of_pages',
+          { pageNumber: pageNumber, pagesCount: pagesCount },
+          '({{pageNumber}} of {{pagesCount}})');
+      } else {
+        items.pageNumber.value = pageNumber;
+      }
+
+      items.previous.disabled = (pageNumber <= 1);
+      items.next.disabled = (pageNumber >= pagesCount);
+
+      items.zoomOut.disabled = (scale <= MIN_SCALE);
+      items.zoomIn.disabled = (scale >= MAX_SCALE);
+
+      selectScaleOption(scaleValue, scale);
+    },
+
+    updateLoadingIndicatorState:
+        function Toolbar_updateLoadingIndicatorState(loading) {
+      var pageNumberInput = this.items.pageNumber;
+
+      if (loading) {
+        pageNumberInput.classList.add(PAGE_NUMBER_LOADING_INDICATOR);
+      } else {
+        pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR);
+      }
+    },
+
+    _adjustScaleWidth: function Toolbar_adjustScaleWidth() {
+      var container = this.items.scaleSelectContainer;
+      var select = this.items.scaleSelect;
+      animationStarted.then(function() {
+        // Adjust the width of the zoom box to fit the content.
+        // Note: If the window is narrow enough that the zoom box is not
+        //       visible, we temporarily show it to be able to adjust its width.
+        if (container.clientWidth === 0) {
+          container.setAttribute('style', 'display: inherit;');
+        }
+        if (container.clientWidth > 0) {
+          select.setAttribute('style', 'min-width: inherit;');
+          var width = select.clientWidth + SCALE_SELECT_CONTAINER_PADDING;
+          select.setAttribute('style', 'min-width: ' +
+                                      (width + SCALE_SELECT_PADDING) + 'px;');
+          container.setAttribute('style', 'min-width: ' + width + 'px; ' +
+                                          'max-width: ' + width + 'px;');
+        }
+      });
+    },
+  };
+
+  return Toolbar;
+})();
+
+exports.Toolbar = Toolbar;
+}));
diff --git a/web/ui_utils.js b/web/ui_utils.js
index 4cf5c3076..9679c4235 100644
--- a/web/ui_utils.js
+++ b/web/ui_utils.js
@@ -28,6 +28,8 @@
 var CSS_UNITS = 96.0 / 72.0;
 var DEFAULT_SCALE_VALUE = 'auto';
 var DEFAULT_SCALE = 1.0;
+var MIN_SCALE = 0.25;
+var MAX_SCALE = 10.0;
 var UNKNOWN_SCALE = 0;
 var MAX_AUTO_SCALE = 1.25;
 var SCROLLBAR_PADDING = 40;
@@ -122,7 +124,7 @@ function getOutputScale(ctx) {
 function scrollIntoView(element, spot, skipOverflowHiddenElements) {
   // Assuming offsetParent is available (it's not available when viewer is in
   // hidden iframe or object). We have to scroll: if the offsetParent is not set
-  // producing the error. See also animationStartedClosure.
+  // producing the error. See also animationStarted.
   var parent = element.offsetParent;
   if (!parent) {
     console.error('offsetParent is not set -- cannot scroll');
@@ -408,6 +410,30 @@ function normalizeWheelEventDelta(evt) {
   return delta;
 }
 
+/**
+ * Promise that is resolved when DOM window becomes visible.
+ */
+var animationStarted = new Promise(function (resolve) {
+  window.requestAnimationFrame(resolve);
+});
+
+/**
+ * Promise that is resolved when UI localization is finished.
+ */
+var localized = new Promise(function (resolve, reject) {
+  if (!mozL10n) {
+    reject(new Error('mozL10n service is not available.'));
+    return;
+  }
+  if (mozL10n.getReadyState() !== 'loading') {
+    resolve();
+    return;
+  }
+  window.addEventListener('localized', function localized(evt) {
+    resolve();
+  });
+});
+
 /**
  * Simple event bus for an application. Listeners are attached using the
  * `on` and `off` methods. To raise an event, the `dispatch` method shall be
@@ -536,6 +562,8 @@ var ProgressBar = (function ProgressBarClosure() {
 exports.CSS_UNITS = CSS_UNITS;
 exports.DEFAULT_SCALE_VALUE = DEFAULT_SCALE_VALUE;
 exports.DEFAULT_SCALE = DEFAULT_SCALE;
+exports.MIN_SCALE = MIN_SCALE;
+exports.MAX_SCALE = MAX_SCALE;
 exports.UNKNOWN_SCALE = UNKNOWN_SCALE;
 exports.MAX_AUTO_SCALE = MAX_AUTO_SCALE;
 exports.SCROLLBAR_PADDING = SCROLLBAR_PADDING;
@@ -554,4 +582,6 @@ exports.scrollIntoView = scrollIntoView;
 exports.watchScroll = watchScroll;
 exports.binarySearchFirstItem = binarySearchFirstItem;
 exports.normalizeWheelEventDelta = normalizeWheelEventDelta;
+exports.animationStarted = animationStarted;
+exports.localized = localized;
 }));
diff --git a/web/viewer.js b/web/viewer.js
index ab620a295..c4fafe512 100644
--- a/web/viewer.js
+++ b/web/viewer.js
@@ -65,8 +65,6 @@ function getViewerConfiguration() {
       customScaleOption: document.getElementById('customScaleOption'),
       previous: document.getElementById('previous'),
       next: document.getElementById('next'),
-      firstPage: document.getElementById('firstPage'),
-      lastPage: document.getElementById('lastPage'),
       zoomIn: document.getElementById('zoomIn'),
       zoomOut: document.getElementById('zoomOut'),
       viewFind: document.getElementById('viewFind'),