From 65c8549759de630ae3c1cf687035a973f45e7afc Mon Sep 17 00:00:00 2001 From: Ryan Hendrickson Date: Mon, 14 May 2018 23:10:32 -0400 Subject: [PATCH 1/6] Fix bug in scrollIntoView Prior to this commit, if the vertical scroll bar is absent and the horizontal scroll bar is present, a link to a particular point on the page which should induce a horizontal scroll did not do so, because the absence of a vertical scroll bar meant that the viewer was not recognized as the nearest scrolling ancestor. This commit adds a check for horizontal scroll bars when searching for the scrolling ancestor. --- web/ui_utils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/ui_utils.js b/web/ui_utils.js index 2ecdcb0ed..f59acab2b 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -115,7 +115,8 @@ function scrollIntoView(element, spot, skipOverflowHiddenElements = false) { } let offsetY = element.offsetTop + element.clientTop; let offsetX = element.offsetLeft + element.clientLeft; - while (parent.clientHeight === parent.scrollHeight || + while ((parent.clientHeight === parent.scrollHeight && + parent.clientWidth === parent.scrollWidth) || (skipOverflowHiddenElements && getComputedStyle(parent).overflow === 'hidden')) { if (parent.dataset._scaleY) { From 91cbc185da8896ea95da3fa2481beff68e5ea48d Mon Sep 17 00:00:00 2001 From: Ryan Hendrickson Date: Mon, 14 May 2018 23:10:32 -0400 Subject: [PATCH 2/6] Add scrolling modes to web viewer In addition to the default scrolling mode (vertical), this commit adds horizontal and wrapped scrolling, implemented primarily with CSS. --- l10n/en-US/viewer.properties | 7 + test/unit/ui_utils_spec.js | 269 +++++++++++++++++- web/app.js | 5 + web/base_viewer.js | 78 ++++- ...econdaryToolbarButton-scrollHorizontal.png | Bin 0 -> 218 bytes ...ndaryToolbarButton-scrollHorizontal@2x.png | Bin 0 -> 332 bytes .../secondaryToolbarButton-scrollVertical.png | Bin 0 -> 228 bytes ...condaryToolbarButton-scrollVertical@2x.png | Bin 0 -> 349 bytes .../secondaryToolbarButton-scrollWrapped.png | Bin 0 -> 297 bytes ...econdaryToolbarButton-scrollWrapped@2x.png | Bin 0 -> 490 bytes web/pdf_viewer.css | 44 +++ web/pdf_viewer.js | 10 +- web/secondary_toolbar.js | 21 +- web/ui_utils.js | 217 +++++++++++++- web/viewer.css | 24 ++ web/viewer.html | 14 +- web/viewer.js | 3 + 17 files changed, 665 insertions(+), 27 deletions(-) create mode 100644 web/images/secondaryToolbarButton-scrollHorizontal.png create mode 100644 web/images/secondaryToolbarButton-scrollHorizontal@2x.png create mode 100644 web/images/secondaryToolbarButton-scrollVertical.png create mode 100644 web/images/secondaryToolbarButton-scrollVertical@2x.png create mode 100644 web/images/secondaryToolbarButton-scrollWrapped.png create mode 100644 web/images/secondaryToolbarButton-scrollWrapped@2x.png diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index 3f4723eb2..dc3bcf06e 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -65,6 +65,13 @@ cursor_text_select_tool_label=Text Selection Tool cursor_hand_tool.title=Enable Hand Tool cursor_hand_tool_label=Hand Tool +scroll_vertical.title=Use Vertical Scrolling +scroll_vertical_label=Vertical Scrolling +scroll_horizontal.title=Use Horizontal Scrolling +scroll_horizontal_label=Horizontal Scrolling +scroll_wrapped.title=Use Wrapped Scrolling +scroll_wrapped_label=Wrapped Scrolling + # Document properties dialog box document_properties.title=Document Properties… document_properties_label=Document Properties… diff --git a/test/unit/ui_utils_spec.js b/test/unit/ui_utils_spec.js index 64426c966..0e93bd8ac 100644 --- a/test/unit/ui_utils_spec.js +++ b/test/unit/ui_utils_spec.js @@ -14,8 +14,10 @@ */ import { - binarySearchFirstItem, EventBus, getPageSizeInches, getPDFFileNameFromURL, - isPortraitOrientation, isValidRotation, waitOnEventOrTimeout, WaitOnType + backtrackBeforeAllVisibleElements, binarySearchFirstItem, EventBus, + getPageSizeInches, getPDFFileNameFromURL, getVisibleElements, + isPortraitOrientation, isValidRotation, moveToEndOfArray, + waitOnEventOrTimeout, WaitOnType } from '../../web/ui_utils'; import { createObjectURL } from '../../src/shared/util'; import isNodeJS from '../../src/shared/is_node'; @@ -447,4 +449,267 @@ describe('ui_utils', function() { expect(height2).toEqual(8.5); }); }); + + describe('getVisibleElements', function() { + // These values are based on margin/border values in the CSS, but there + // isn't any real need for them to be; they just need to take *some* value. + const BORDER_WIDTH = 9; + const SPACING = 2 * BORDER_WIDTH - 7; + + // This is a helper function for assembling an array of view stubs from an + // array of arrays of [width, height] pairs, which represents wrapped lines + // of pages. It uses the above constants to add realistic spacing between + // the pages and the lines. + // + // If you're reading a test that calls makePages, you should think of the + // inputs to makePages as boxes with no borders, being laid out in a + // container that has no margins, so that the top of the tallest page in + // the first row will be at y = 0, and the left of the first page in + // the first row will be at x = 0. The spacing between pages in a row, and + // the spacing between rows, is SPACING. If you wanted to construct an + // actual HTML document with the same layout, you should give each page + // element a margin-right and margin-bottom of SPACING, and add no other + // margins, borders, or padding. + // + // If you're reading makePages itself, you'll see a somewhat more + // complicated picture because this suite of tests is exercising + // getVisibleElements' ability to account for the borders that real page + // elements have. makePages tests this by subtracting a BORDER_WIDTH from + // offsetLeft/Top and adding it to clientLeft/Top. So the element stubs that + // getVisibleElements sees may, for example, actually have an offsetTop of + // -9. If everything is working correctly, this detail won't leak out into + // the tests themselves, and so the tests shouldn't use the value of + // BORDER_WIDTH at all. + function makePages(lines) { + const result = []; + let lineTop = 0, id = 0; + for (const line of lines) { + const lineHeight = line.reduce(function(maxHeight, pair) { + return Math.max(maxHeight, pair[1]); + }, 0); + let offsetLeft = -BORDER_WIDTH; + for (const [clientWidth, clientHeight] of line) { + const offsetTop = + lineTop + (lineHeight - clientHeight) / 2 - BORDER_WIDTH; + const div = { + offsetLeft, offsetTop, clientWidth, clientHeight, + clientLeft: BORDER_WIDTH, clientTop: BORDER_WIDTH, + }; + result.push({ id, div, }); + ++id; + offsetLeft += clientWidth + SPACING; + } + lineTop += lineHeight + SPACING; + } + return result; + } + + // This is a reimplementation of getVisibleElements without the + // optimizations. + function slowGetVisibleElements(scroll, pages) { + const views = []; + const { scrollLeft, scrollTop, } = scroll; + const scrollRight = scrollLeft + scroll.clientWidth; + const scrollBottom = scrollTop + scroll.clientHeight; + for (const view of pages) { + const { div, } = view; + const viewLeft = div.offsetLeft + div.clientLeft; + const viewRight = viewLeft + div.clientWidth; + const viewTop = div.offsetTop + div.clientTop; + const viewBottom = viewTop + div.clientHeight; + + if (viewLeft < scrollRight && viewRight > scrollLeft && + viewTop < scrollBottom && viewBottom > scrollTop) { + const hiddenHeight = Math.max(0, scrollTop - viewTop) + + Math.max(0, viewBottom - scrollBottom); + const hiddenWidth = Math.max(0, scrollLeft - viewLeft) + + Math.max(0, viewRight - scrollRight); + const visibleArea = (div.clientHeight - hiddenHeight) * + (div.clientWidth - hiddenWidth); + const percent = + (visibleArea * 100 / div.clientHeight / div.clientWidth) | 0; + views.push({ id: view.id, x: viewLeft, y: viewTop, view, percent, }); + } + } + return { first: views[0], last: views[views.length - 1], views, }; + } + + // This function takes a fixed layout of pages and compares the system under + // test to the slower implementation above, for a range of scroll viewport + // sizes and positions. + function scrollOverDocument(pages, horizontally = false) { + const size = pages.reduce(function(max, { div, }) { + return Math.max( + max, + horizontally ? + div.offsetLeft + div.clientLeft + div.clientWidth : + div.offsetTop + div.clientTop + div.clientHeight); + }, 0); + // The numbers (7 and 5) are mostly arbitrary, not magic: increase them to + // make scrollOverDocument tests faster, decrease them to make the tests + // more scrupulous, and keep them coprime to reduce the chance of missing + // weird edge case bugs. + for (let i = 0; i < size; i += 7) { + // The screen height (or width) here (j - i) doubles on each inner loop + // iteration; again, this is just to test an interesting range of cases + // without slowing the tests down to check every possible case. + for (let j = i + 5; j < size; j += (j - i)) { + const scroll = horizontally ? { + scrollTop: 0, + scrollLeft: i, + clientHeight: 10000, + clientWidth: j - i, + } : { + scrollTop: i, + scrollLeft: 0, + clientHeight: j - i, + clientWidth: 10000, + }; + expect(getVisibleElements(scroll, pages, false, horizontally)) + .toEqual(slowGetVisibleElements(scroll, pages)); + } + } + } + + it('with pages of varying height', function() { + const pages = makePages([ + [[50, 20], [20, 50]], + [[30, 12], [12, 30]], + [[20, 50], [50, 20]], + [[50, 20], [20, 50]], + ]); + scrollOverDocument(pages); + }); + + it('widescreen challenge', function() { + const pages = makePages([ + [[10, 50], [10, 60], [10, 70], [10, 80], [10, 90]], + [[10, 90], [10, 80], [10, 70], [10, 60], [10, 50]], + [[10, 50], [10, 60], [10, 70], [10, 80], [10, 90]], + ]); + scrollOverDocument(pages); + }); + + it('works with horizontal scrolling', function() { + const pages = makePages([ + [[10, 50], [20, 20], [30, 10]], + ]); + scrollOverDocument(pages, true); + }); + + // This sub-suite is for a notionally internal helper function for + // getVisibleElements. + describe('backtrackBeforeAllVisibleElements', function() { + // Layout elements common to all tests + const tallPage = [10, 50]; + const shortPage = [10, 10]; + + // A scroll position that ensures that only the tall pages in the second + // row are visible + const top1 = + 20 + SPACING + // height of the first row + 40; // a value between 30 (so the short pages on the second row are + // hidden) and 50 (so the tall pages are visible) + + // A scroll position that ensures that all of the pages in the second row + // are visible, but the tall ones are a tiny bit cut off + const top2 = 20 + SPACING + // height of the first row + 10; // a value greater than 0 but less than 30 + + // These tests refer to cases enumerated in the comments of + // backtrackBeforeAllVisibleElements. + it('handles case 1', function() { + const pages = makePages([ + [[10, 20], [10, 20], [10, 20], [10, 20]], + [tallPage, shortPage, tallPage, shortPage], + [[10, 50], [10, 50], [10, 50], [10, 50]], + [[10, 20], [10, 20], [10, 20], [10, 20]], + [[10, 20]], + ]); + // binary search would land on the second row, first page + const bsResult = 4; + expect(backtrackBeforeAllVisibleElements(bsResult, pages, top1)) + .toEqual(4); + }); + + it('handles case 2', function() { + const pages = makePages([ + [[10, 20], [10, 20], [10, 20], [10, 20]], + [tallPage, shortPage, tallPage, tallPage], + [[10, 50], [10, 50], [10, 50], [10, 50]], + [[10, 20], [10, 20], [10, 20], [10, 20]], + ]); + // binary search would land on the second row, third page + const bsResult = 6; + expect(backtrackBeforeAllVisibleElements(bsResult, pages, top1)) + .toEqual(4); + }); + + it('handles case 3', function() { + const pages = makePages([ + [[10, 20], [10, 20], [10, 20], [10, 20]], + [tallPage, shortPage, tallPage, shortPage], + [[10, 50], [10, 50], [10, 50], [10, 50]], + [[10, 20], [10, 20], [10, 20], [10, 20]], + ]); + // binary search would land on the third row, first page + const bsResult = 8; + expect(backtrackBeforeAllVisibleElements(bsResult, pages, top1)) + .toEqual(4); + }); + + it('handles case 4', function() { + const pages = makePages([ + [[10, 20], [10, 20], [10, 20], [10, 20]], + [tallPage, shortPage, tallPage, shortPage], + [[10, 50], [10, 50], [10, 50], [10, 50]], + [[10, 20], [10, 20], [10, 20], [10, 20]], + ]); + // binary search would land on the second row, first page + const bsResult = 4; + expect(backtrackBeforeAllVisibleElements(bsResult, pages, top2)) + .toEqual(4); + }); + }); + }); + + describe('moveToEndOfArray', function() { + it('works on empty arrays', function() { + const data = []; + moveToEndOfArray(data, function() {}); + expect(data).toEqual([]); + }); + + it('works when moving everything', function() { + const data = [1, 2, 3, 4, 5]; + moveToEndOfArray(data, function() { + return true; + }); + expect(data).toEqual([1, 2, 3, 4, 5]); + }); + + it('works when moving some things', function() { + const data = [1, 2, 3, 4, 5]; + moveToEndOfArray(data, function(x) { + return x % 2 === 0; + }); + expect(data).toEqual([1, 3, 5, 2, 4]); + }); + + it('works when moving one thing', function() { + const data = [1, 2, 3, 4, 5]; + moveToEndOfArray(data, function(x) { + return x === 1; + }); + expect(data).toEqual([2, 3, 4, 5, 1]); + }); + + it('works when moving nothing', function() { + const data = [1, 2, 3, 4, 5]; + moveToEndOfArray(data, function(x) { + return x === 0; + }); + expect(data).toEqual([1, 2, 3, 4, 5]); + }); + }); }); diff --git a/web/app.js b/web/app.js index 4cb950899..705c1a523 100644 --- a/web/app.js +++ b/web/app.js @@ -1385,6 +1385,7 @@ let PDFViewerApplication = { eventBus.on('scalechanged', webViewerScaleChanged); eventBus.on('rotatecw', webViewerRotateCw); eventBus.on('rotateccw', webViewerRotateCcw); + eventBus.on('switchscrollmode', webViewerSwitchScrollMode); eventBus.on('documentproperties', webViewerDocumentProperties); eventBus.on('find', webViewerFind); eventBus.on('findfromurlhash', webViewerFindFromUrlHash); @@ -1451,6 +1452,7 @@ let PDFViewerApplication = { eventBus.off('scalechanged', webViewerScaleChanged); eventBus.off('rotatecw', webViewerRotateCw); eventBus.off('rotateccw', webViewerRotateCcw); + eventBus.off('switchscrollmode', webViewerSwitchScrollMode); eventBus.off('documentproperties', webViewerDocumentProperties); eventBus.off('find', webViewerFind); eventBus.off('findfromurlhash', webViewerFindFromUrlHash); @@ -1960,6 +1962,9 @@ function webViewerRotateCw() { function webViewerRotateCcw() { PDFViewerApplication.rotatePages(-90); } +function webViewerSwitchScrollMode(evt) { + PDFViewerApplication.pdfViewer.setScrollMode(evt.mode); +} function webViewerDocumentProperties() { PDFViewerApplication.pdfDocumentProperties.open(); } diff --git a/web/base_viewer.js b/web/base_viewer.js index daefce3cc..94de1779c 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -15,9 +15,9 @@ import { CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, isPortraitOrientation, - isValidRotation, MAX_AUTO_SCALE, NullL10n, PresentationModeState, - RendererType, SCROLLBAR_PADDING, TextLayerMode, UNKNOWN_SCALE, - VERTICAL_PADDING, watchScroll + isValidRotation, MAX_AUTO_SCALE, moveToEndOfArray, NullL10n, + PresentationModeState, RendererType, SCROLLBAR_PADDING, TextLayerMode, + UNKNOWN_SCALE, VERTICAL_PADDING, watchScroll } from './ui_utils'; import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue'; import { AnnotationLayerBuilder } from './annotation_layer_builder'; @@ -29,6 +29,12 @@ import { TextLayerBuilder } from './text_layer_builder'; const DEFAULT_CACHE_SIZE = 10; +const ScrollMode = { + VERTICAL: 0, // The default value. + HORIZONTAL: 1, + WRAPPED: 2, +}; + /** * @typedef {Object} PDFViewerOptions * @property {HTMLDivElement} container - The container for the viewer element. @@ -61,6 +67,10 @@ const DEFAULT_CACHE_SIZE = 10; * size in total pixels, i.e. width * height. Use -1 for no limit. * The default value is 4096 * 4096 (16 mega-pixels). * @property {IL10n} l10n - Localization service. + * @property {number} scrollMode - (optional) The direction in which the + * document pages should be laid out within the scrolling container. The + * constants from {ScrollMode} should be used. The default value is + * `ScrollMode.VERTICAL`. */ function PDFPageViewBuffer(size) { @@ -75,8 +85,24 @@ function PDFPageViewBuffer(size) { data.shift().destroy(); } }; - this.resize = function(newSize) { + /** + * After calling resize, the size of the buffer will be newSize. The optional + * parameter pagesToKeep is, if present, an array of pages to push to the back + * of the buffer, delaying their destruction. The size of pagesToKeep has no + * impact on the final size of the buffer; if pagesToKeep has length larger + * than newSize, some of those pages will be destroyed anyway. + */ + this.resize = function(newSize, pagesToKeep) { size = newSize; + if (pagesToKeep) { + const pageIdsToKeep = new Set(); + for (let i = 0, iMax = pagesToKeep.length; i < iMax; ++i) { + pageIdsToKeep.add(pagesToKeep[i].id); + } + moveToEndOfArray(data, function(page) { + return pageIdsToKeep.has(page.id); + }); + } while (data.length > size) { data.shift().destroy(); } @@ -126,6 +152,7 @@ class BaseViewer { this.useOnlyCssZoom = options.useOnlyCssZoom || false; this.maxCanvasPixels = options.maxCanvasPixels; this.l10n = options.l10n || NullL10n; + this.scrollMode = options.scrollMode || ScrollMode.VERTICAL; this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { @@ -143,6 +170,7 @@ class BaseViewer { if (this.removePageBorders) { this.viewer.classList.add('removePageBorders'); } + this._updateScrollModeClasses(); } get pagesCount() { @@ -557,6 +585,11 @@ class BaseViewer { 0 : SCROLLBAR_PADDING; let vPadding = (this.isInPresentationMode || this.removePageBorders) ? 0 : VERTICAL_PADDING; + if (this.scrollMode === ScrollMode.HORIZONTAL) { + const temp = hPadding; + hPadding = vPadding; + vPadding = temp; + } let pageWidthScale = (this.container.clientWidth - hPadding) / currentPage.width * currentPage.scale; let pageHeightScale = (this.container.clientHeight - vPadding) / @@ -733,10 +766,15 @@ class BaseViewer { }); } - _resizeBuffer(numVisiblePages) { + /** + * visiblePages is optional; if present, it should be an array of pages and in + * practice its length is going to be numVisiblePages, but this is not + * required. The new size of the buffer depends only on numVisiblePages. + */ + _resizeBuffer(numVisiblePages, visiblePages) { let suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1); - this._buffer.resize(suggestedCacheSize); + this._buffer.resize(suggestedCacheSize, visiblePages); } _updateLocation(firstPage) { @@ -847,9 +885,11 @@ class BaseViewer { forceRendering(currentlyVisiblePages) { let visiblePages = currentlyVisiblePages || this._getVisiblePages(); + let scrollAhead = this.scrollMode === ScrollMode.HORIZONTAL ? + this.scroll.right : this.scroll.down; let pageView = this.renderingQueue.getHighestPriority(visiblePages, this._pages, - this.scroll.down); + scrollAhead); if (pageView) { this._ensurePdfPageLoaded(pageView).then(() => { this.renderingQueue.renderView(pageView); @@ -957,8 +997,32 @@ class BaseViewer { }; }); } + + setScrollMode(mode) { + if (mode !== this.scrollMode) { + this.scrollMode = mode; + this._updateScrollModeClasses(); + this.eventBus.dispatch('scrollmodechanged', { mode, }); + const pageNumber = this._currentPageNumber; + // Non-numeric scale modes can be sensitive to the scroll orientation. + // Call this before re-scrolling to the current page, to ensure that any + // changes in scale don't move the current page. + if (isNaN(this._currentScaleValue)) { + this._setScale(this._currentScaleValue, this.isInPresentationMode); + } + this.scrollPageIntoView({ pageNumber, }); + this.update(); + } + } + + _updateScrollModeClasses() { + const mode = this.scrollMode, { classList, } = this.viewer; + classList.toggle('scrollHorizontal', mode === ScrollMode.HORIZONTAL); + classList.toggle('scrollWrapped', mode === ScrollMode.WRAPPED); + } } export { BaseViewer, + ScrollMode, }; diff --git a/web/images/secondaryToolbarButton-scrollHorizontal.png b/web/images/secondaryToolbarButton-scrollHorizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..cb702fc4d11149a31f22495ad285b98cc3cf21c4 GIT binary patch literal 218 zcmV<0044v4P)*%Ckqm%#nzTkbB2o8#L5J}@;sIu@+PaMFaH&X@x>v-#* zT%ew8%}^g;^@P+XsOmg~P#3(NpsFc^aQ}m07`CO9Heo%E<1FX=-1y2jJ^yvx04Rl6 U>n3b|U;qFB07*qoM6N<$f*JE%`Tzg` literal 0 HcmV?d00001 diff --git a/web/images/secondaryToolbarButton-scrollHorizontal@2x.png b/web/images/secondaryToolbarButton-scrollHorizontal@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7f05289bb157e3a8bb79578e556ff6c886c9d57b GIT binary patch literal 332 zcmV-S0ki&zP)P2JUa$YvE|?$&qg2(0Xi}8 zfoQyg@~LjHi+wr*R{+R4v(^@qYf;}3C1d=Jb-(>N~acHj9o8x-B5D`bbPkWsoqwjXW=vA#*Zmj-Z%P*2A>KL^Q|OZ0(MBMLPdkr?-{ z27ne&b6PP}y`|PHi1mC~N<%9EN$j)cHxNr0th8Q2!`SGRxh1(e4YT!t_x?9B|Fu34 z0HOCSdjf6S?jL&K?)AV2fVYV_V>Wz|+?J-{2EZ}1^KEL)g2guX$3!N|z)Jzp?YPJZ vSpfVe-mcm-OoT_~Nu^5~qO z_Vl`zF1R?>lAa|fmnyjZ_yu(i?l0-Q#UsEpO`prM4Cmk~zttRk0ipk+h*XpK1rZ4>56aa00000NkvXXu0mjftGk6u literal 0 HcmV?d00001 diff --git a/web/images/secondaryToolbarButton-scrollWrapped@2x.png b/web/images/secondaryToolbarButton-scrollWrapped@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4246141192214e00d2b789685bd28591e355a40d GIT binary patch literal 490 zcmVV)ZSj4_BA!t|BfA0p6o-NP|_ z-gVvMX&(6Z^S~1kT|0VtCO-qX6Ljp^OL+lsE6cDYqFrU@YmK#CC4glu7fcbJ(6);Q z6Yo$_>hG)^ZscgEazTmFN)bvG{NU2@hjZkb%Z9AYlNJDLDAB?PXXKK=Q!O2*!UzUO gM}`~DfHTU0Py7T36}?uA&j0`b07*qoM6N<$f)Qxjl>h($ literal 0 HcmV?d00001 diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index cc0afa1fb..a0adbde3f 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -46,6 +46,35 @@ border: none; } +.pdfViewer.scrollHorizontal, .pdfViewer.scrollWrapped { + margin-left: 3.5px; + margin-right: 3.5px; + text-align: center; +} + +.pdfViewer.scrollHorizontal { + white-space: nowrap; +} + +.pdfViewer.removePageBorders { + margin-left: 0; + margin-right: 0; +} + +.pdfViewer.scrollHorizontal .page, +.pdfViewer.scrollWrapped .page { + display: inline-block; + margin-left: -3.5px; + margin-right: -3.5px; + vertical-align: middle; +} + +.pdfViewer.removePageBorders.scrollHorizontal .page, +.pdfViewer.removePageBorders.scrollWrapped .page { + margin-left: 5px; + margin-right: 5px; +} + .pdfViewer .page canvas { margin: 0; display: block; @@ -65,6 +94,21 @@ background: url('images/loading-icon.gif') center no-repeat; } +.pdfPresentationMode .pdfViewer { + margin-left: 0; + margin-right: 0; +} + +.pdfPresentationMode .pdfViewer .page { + display: block; +} + +.pdfPresentationMode .pdfViewer .page, +.pdfPresentationMode .pdfViewer.removePageBorders .page { + margin-left: auto; + margin-right: auto; +} + .pdfPresentationMode:-ms-fullscreen .pdfViewer .page { margin-bottom: 100% !important; } diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index c32065ff6..e6e520f5f 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -13,8 +13,8 @@ * limitations under the License. */ +import { BaseViewer, ScrollMode } from './base_viewer'; import { getVisibleElements, scrollIntoView } from './ui_utils'; -import { BaseViewer } from './base_viewer'; import { shadow } from 'pdfjs-lib'; class PDFViewer extends BaseViewer { @@ -23,12 +23,16 @@ class PDFViewer extends BaseViewer { } _scrollIntoView({ pageDiv, pageSpot = null, }) { + if (!pageSpot && this.scrollMode === ScrollMode.HORIZONTAL) { + pageSpot = { left: 0, top: 0, }; + } scrollIntoView(pageDiv, pageSpot); } _getVisiblePages() { if (!this.isInPresentationMode) { - return getVisibleElements(this.container, this._pages, true); + return getVisibleElements(this.container, this._pages, true, + this.scrollMode === ScrollMode.HORIZONTAL); } // The algorithm in getVisibleElements doesn't work in all browsers and // configurations when presentation mode is active. @@ -44,7 +48,7 @@ class PDFViewer extends BaseViewer { if (numVisiblePages === 0) { return; } - this._resizeBuffer(numVisiblePages); + this._resizeBuffer(numVisiblePages, visiblePages); this.renderingQueue.renderHighestPriority(visible); diff --git a/web/secondary_toolbar.js b/web/secondary_toolbar.js index 6495fc5ef..a332f668b 100644 --- a/web/secondary_toolbar.js +++ b/web/secondary_toolbar.js @@ -15,6 +15,7 @@ import { CursorTool } from './pdf_cursor_tools'; import { SCROLLBAR_PADDING } from './ui_utils'; +import { ScrollMode } from './base_viewer'; /** * @typedef {Object} SecondaryToolbarOptions @@ -76,6 +77,12 @@ class SecondaryToolbar { eventDetails: { tool: CursorTool.SELECT, }, close: true, }, { element: options.cursorHandToolButton, eventName: 'switchcursortool', eventDetails: { tool: CursorTool.HAND, }, close: true, }, + { element: options.scrollVerticalButton, eventName: 'switchscrollmode', + eventDetails: { mode: ScrollMode.VERTICAL, }, close: true, }, + { element: options.scrollHorizontalButton, eventName: 'switchscrollmode', + eventDetails: { mode: ScrollMode.HORIZONTAL, }, close: true, }, + { element: options.scrollWrappedButton, eventName: 'switchscrollmode', + eventDetails: { mode: ScrollMode.WRAPPED, }, close: true, }, { element: options.documentPropertiesButton, eventName: 'documentproperties', close: true, }, ]; @@ -95,9 +102,10 @@ class SecondaryToolbar { this.reset(); - // Bind the event listeners for click and cursor tool actions. + // Bind the event listeners for click, cursor tool, and scroll mode actions. this._bindClickListeners(); this._bindCursorToolsListener(options); + this._bindScrollModeListener(options); // Bind the event listener for adjusting the 'max-height' of the toolbar. this.eventBus.on('resize', this._setMaxHeight.bind(this)); @@ -172,6 +180,17 @@ class SecondaryToolbar { }); } + _bindScrollModeListener(buttons) { + this.eventBus.on('scrollmodechanged', function(evt) { + buttons.scrollVerticalButton.classList.toggle('toggled', + evt.mode === ScrollMode.VERTICAL); + buttons.scrollHorizontalButton.classList.toggle('toggled', + evt.mode === ScrollMode.HORIZONTAL); + buttons.scrollWrappedButton.classList.toggle('toggled', + evt.mode === ScrollMode.WRAPPED); + }); + } + open() { if (this.opened) { return; diff --git a/web/ui_utils.js b/web/ui_utils.js index f59acab2b..b38489341 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -155,6 +155,12 @@ function watchScroll(viewAreaElement, callback) { rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { rAF = null; + let currentX = viewAreaElement.scrollLeft; + let lastX = state.lastX; + if (currentX !== lastX) { + state.right = currentX > lastX; + } + state.lastX = currentX; let currentY = viewAreaElement.scrollTop; let lastY = state.lastY; if (currentY !== lastY) { @@ -166,7 +172,9 @@ function watchScroll(viewAreaElement, callback) { }; let state = { + right: true, down: true, + lastX: viewAreaElement.scrollLeft, lastY: viewAreaElement.scrollTop, _eventHandler: debounceScroll, }; @@ -296,50 +304,211 @@ function getPageSizeInches({ view, userUnit, rotate, }) { } /** - * Generic helper to find out what elements are visible within a scroll pane. + * Helper function for getVisibleElements. + * + * @param {number} index - initial guess at the first visible element + * @param {Array} views - array of pages, into which `index` is an index + * @param {number} top - the top of the scroll pane + * @returns {number} less than or equal to `index` that is definitely at or + * before the first visible element in `views`, but not by too much. (Usually, + * this will be the first element in the first partially visible row in + * `views`, although sometimes it goes back one row further.) */ -function getVisibleElements(scrollEl, views, sortByVisibility = false) { +function backtrackBeforeAllVisibleElements(index, views, top) { + // binarySearchFirstItem's assumption is that the input is ordered, with only + // one index where the conditions flips from false to true: + // [false ..., true...]. With wrapped scrolling, it is possible to have + // [false ..., true, false, true ...]. + // + // So there is no guarantee that the binary search yields the index of the + // first visible element. It could have been any of the other visible elements + // that were preceded by a hidden element. + + // Of course, if either this element or the previous (hidden) element is also + // the first element, there's nothing to worry about. + if (index < 2) { + return index; + } + + // That aside, the possible cases are represented below. + // + // **** = fully hidden + // A*B* = mix of partially visible and/or hidden pages + // CDEF = fully visible + // + // (1) Binary search could have returned A, in which case we can stop. + // (2) Binary search could also have returned B, in which case we need to + // check the whole row. + // (3) Binary search could also have returned C, in which case we need to + // check the whole previous row. + // + // There's one other possibility: + // + // **** = fully hidden + // ABCD = mix of fully and/or partially visible pages + // + // (4) Binary search could only have returned A. + + // Initially assume that we need to find the beginning of the current row + // (case 1, 2, or 4), which means finding a page that is above the current + // page's top. If the found page is partially visible, we're definitely not in + // case 3, and this assumption is correct. + let elt = views[index].div; + let pageTop = elt.offsetTop + elt.clientTop; + + if (pageTop >= top) { + // The found page is fully visible, so we're actually either in case 3 or 4, + // and unfortunately we can't tell the difference between them without + // scanning the entire previous row, so we just conservatively assume that + // we do need to backtrack to that row. In both cases, the previous page is + // in the previous row, so use its top instead. + elt = views[index - 1].div; + pageTop = elt.offsetTop + elt.clientTop; + } + + // Now we backtrack to the first page that still has its bottom below + // `pageTop`, which is the top of a page in the first visible row (unless + // we're in case 4, in which case it's the row before that). + // `index` is found by binary search, so the page at `index - 1` is + // invisible and we can start looking for potentially visible pages from + // `index - 2`. (However, if this loop terminates on its first iteration, + // which is the case when pages are stacked vertically, `index` should remain + // unchanged, so we use a distinct loop variable.) + for (let i = index - 2; i >= 0; --i) { + elt = views[i].div; + if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) { + // We have reached the previous row, so stop now. + // This loop is expected to terminate relatively quickly because the + // number of pages per row is expected to be small. + break; + } + index = i; + } + return index; +} + +/** + * Generic helper to find out what elements are visible within a scroll pane. + * + * Well, pretty generic. There are some assumptions placed on the elements + * referenced by `views`: + * - If `horizontal`, no left of any earlier element is to the right of the + * left of any later element. + * - Otherwise, `views` can be split into contiguous rows where, within a row, + * no top of any element is below the bottom of any other element, and + * between rows, no bottom of any element in an earlier row is below the + * top of any element in a later row. + * + * (Here, top, left, etc. all refer to the padding edge of the element in + * question. For pages, that ends up being equivalent to the bounding box of the + * rendering canvas. Earlier and later refer to index in `views`, not page + * layout.) + * + * @param scrollEl {HTMLElement} - a container that can possibly scroll + * @param views {Array} - objects with a `div` property that contains an + * HTMLElement, which should all be descendents of `scrollEl` satisfying the + * above layout assumptions + * @param sortByVisibility {boolean} - if true, the returned elements are sorted + * in descending order of the percent of their padding box that is visible + * @param horizontal {boolean} - if true, the elements are assumed to be laid + * out horizontally instead of vertically + * @returns {Object} `{ first, last, views: [{ id, x, y, view, percent }] }` + */ +function getVisibleElements(scrollEl, views, sortByVisibility = false, + horizontal = false) { let top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; let left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; - function isElementBottomBelowViewTop(view) { + // Throughout this "generic" function, comments will assume we're working with + // PDF document pages, which is the most important and complex case. In this + // case, the visible elements we're actually interested is the page canvas, + // which is contained in a wrapper which adds no padding/border/margin, which + // is itself contained in `view.div` which adds no padding (but does add a + // border). So, as specified in this function's doc comment, this function + // does all of its work on the padding edge of the provided views, starting at + // offsetLeft/Top (which includes margin) and adding clientLeft/Top (which is + // the border). Adding clientWidth/Height gets us the bottom-right corner of + // the padding edge. + function isElementBottomAfterViewTop(view) { let element = view.div; let elementBottom = element.offsetTop + element.clientTop + element.clientHeight; return elementBottom > top; } + function isElementRightAfterViewLeft(view) { + let element = view.div; + let elementRight = + element.offsetLeft + element.clientLeft + element.clientWidth; + return elementRight > left; + } let visible = [], view, element; - let currentHeight, viewHeight, hiddenHeight, percentHeight; - let currentWidth, viewWidth; + let currentHeight, viewHeight, viewBottom, hiddenHeight; + let currentWidth, viewWidth, viewRight, hiddenWidth; + let percentVisible; let firstVisibleElementInd = views.length === 0 ? 0 : - binarySearchFirstItem(views, isElementBottomBelowViewTop); + binarySearchFirstItem(views, horizontal ? isElementRightAfterViewLeft : + isElementBottomAfterViewTop); + + if (views.length > 0 && !horizontal) { + // In wrapped scrolling, with some page sizes, isElementBottomAfterViewTop + // doesn't satisfy the binary search condition: there can be pages with + // bottoms above the view top between pages with bottoms below. This + // function detects and corrects that error; see it for more comments. + firstVisibleElementInd = + backtrackBeforeAllVisibleElements(firstVisibleElementInd, views, top); + } + + // lastEdge acts as a cutoff for us to stop looping, because we know all + // subsequent pages will be hidden. + // + // When using wrapped scrolling, we can't simply stop the first time we reach + // a page below the bottom of the view; the tops of subsequent pages on the + // same row could still be visible. In horizontal scrolling, we don't have + // that issue, so we can stop as soon as we pass `right`, without needing the + // code below that handles the -1 case. + let lastEdge = horizontal ? right : -1; for (let i = firstVisibleElementInd, ii = views.length; i < ii; i++) { view = views[i]; element = view.div; + currentWidth = element.offsetLeft + element.clientLeft; currentHeight = element.offsetTop + element.clientTop; + viewWidth = element.clientWidth; viewHeight = element.clientHeight; + viewRight = currentWidth + viewWidth; + viewBottom = currentHeight + viewHeight; - if (currentHeight > bottom) { + if (lastEdge === -1) { + // As commented above, this is only needed in non-horizontal cases. + // Setting lastEdge to the bottom of the first page that is partially + // visible ensures that the next page fully below lastEdge is on the + // next row, which has to be fully hidden along with all subsequent rows. + if (viewBottom >= bottom) { + lastEdge = viewBottom; + } + } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) { break; } - currentWidth = element.offsetLeft + element.clientLeft; - viewWidth = element.clientWidth; - if (currentWidth + viewWidth < left || currentWidth > right) { + if (viewBottom <= top || currentHeight >= bottom || + viewRight <= left || currentWidth >= right) { continue; } + hiddenHeight = Math.max(0, top - currentHeight) + - Math.max(0, currentHeight + viewHeight - bottom); - percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0; + Math.max(0, viewBottom - bottom); + hiddenWidth = Math.max(0, left - currentWidth) + + Math.max(0, viewRight - right); + percentVisible = ((viewHeight - hiddenHeight) * (viewWidth - hiddenWidth) * + 100 / viewHeight / viewWidth) | 0; visible.push({ id: view.id, x: currentWidth, y: currentHeight, view, - percent: percentHeight, + percent: percentVisible, }); } @@ -640,6 +809,26 @@ class ProgressBar { } } +/** + * Moves all elements of an array that satisfy condition to the end of the + * array, preserving the order of the rest. + */ +function moveToEndOfArray(arr, condition) { + const moved = [], len = arr.length; + let write = 0; + for (let read = 0; read < len; ++read) { + if (condition(arr[read])) { + moved.push(arr[read]); + } else { + arr[write] = arr[read]; + ++write; + } + } + for (let read = 0; write < len; ++read, ++write) { + arr[write] = moved[read]; + } +} + export { CSS_UNITS, DEFAULT_SCALE_VALUE, @@ -663,6 +852,7 @@ export { getPDFFileNameFromURL, noContextMenuHandler, parseQueryString, + backtrackBeforeAllVisibleElements, // only exported for testing getVisibleElements, roundToDivide, getPageSizeInches, @@ -675,4 +865,5 @@ export { animationStarted, WaitOnType, waitOnEventOrTimeout, + moveToEndOfArray, }; diff --git a/web/viewer.css b/web/viewer.css index 0b008d92b..838ba81a6 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -966,6 +966,18 @@ html[dir="rtl"] .secondaryToolbarButton > span { content: url(images/secondaryToolbarButton-handTool.png); } +.secondaryToolbarButton.scrollVertical::before { + content: url(images/secondaryToolbarButton-scrollVertical.png); +} + +.secondaryToolbarButton.scrollHorizontal::before { + content: url(images/secondaryToolbarButton-scrollHorizontal.png); +} + +.secondaryToolbarButton.scrollWrapped::before { + content: url(images/secondaryToolbarButton-scrollWrapped.png); +} + .secondaryToolbarButton.documentProperties::before { content: url(images/secondaryToolbarButton-documentProperties.png); } @@ -1689,6 +1701,18 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { content: url(images/secondaryToolbarButton-handTool@2x.png); } + .secondaryToolbarButton.scrollVertical::before { + content: url(images/secondaryToolbarButton-scrollVertical@2x.png); + } + + .secondaryToolbarButton.scrollHorizontal::before { + content: url(images/secondaryToolbarButton-scrollHorizontal@2x.png); + } + + .secondaryToolbarButton.scrollWrapped::before { + content: url(images/secondaryToolbarButton-scrollWrapped@2x.png); + } + .secondaryToolbarButton.documentProperties::before { content: url(images/secondaryToolbarButton-documentProperties@2x.png); } diff --git a/web/viewer.html b/web/viewer.html index 2d460456c..e92592e1a 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -168,7 +168,19 @@ See https://github.com/adobe-type-tools/cmap-resources
- + + + +
+ + diff --git a/web/viewer.js b/web/viewer.js index 632a217f7..8206b9008 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -96,6 +96,9 @@ function getViewerConfiguration() { pageRotateCcwButton: document.getElementById('pageRotateCcw'), cursorSelectToolButton: document.getElementById('cursorSelectTool'), cursorHandToolButton: document.getElementById('cursorHandTool'), + scrollVerticalButton: document.getElementById('scrollVertical'), + scrollHorizontalButton: document.getElementById('scrollHorizontal'), + scrollWrappedButton: document.getElementById('scrollWrapped'), documentPropertiesButton: document.getElementById('documentProperties'), }, fullscreen: { From 3d83c646c6b9ff8046a8a1485e170e088ed6fbaf Mon Sep 17 00:00:00 2001 From: Ryan Hendrickson Date: Mon, 14 May 2018 23:10:32 -0400 Subject: [PATCH 3/6] Add spread modes to web viewer This builds on the scrolling mode work to add three buttons for joining page spreads together: one for the default view, with no page spreads, and two for spreads starting on odd-numbered or even-numbered pages. --- l10n/en-US/viewer.properties | 7 +++ web/app.js | 5 +++ web/base_viewer.js | 25 +++++++++++ .../secondaryToolbarButton-spreadEven.png | Bin 0 -> 347 bytes .../secondaryToolbarButton-spreadEven@2x.png | Bin 0 -> 694 bytes .../secondaryToolbarButton-spreadNone.png | Bin 0 -> 179 bytes .../secondaryToolbarButton-spreadNone@2x.png | Bin 0 -> 261 bytes .../secondaryToolbarButton-spreadOdd.png | Bin 0 -> 344 bytes .../secondaryToolbarButton-spreadOdd@2x.png | Bin 0 -> 621 bytes web/pdf_viewer.css | 26 +++++++++--- web/pdf_viewer.js | 40 ++++++++++++++++-- web/secondary_toolbar.js | 23 +++++++++- web/ui_utils.js | 26 ++++++------ web/viewer.css | 24 +++++++++++ web/viewer.html | 14 +++++- web/viewer.js | 3 ++ 16 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 web/images/secondaryToolbarButton-spreadEven.png create mode 100644 web/images/secondaryToolbarButton-spreadEven@2x.png create mode 100644 web/images/secondaryToolbarButton-spreadNone.png create mode 100644 web/images/secondaryToolbarButton-spreadNone@2x.png create mode 100644 web/images/secondaryToolbarButton-spreadOdd.png create mode 100644 web/images/secondaryToolbarButton-spreadOdd@2x.png diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index dc3bcf06e..af1765fc1 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -72,6 +72,13 @@ scroll_horizontal_label=Horizontal Scrolling scroll_wrapped.title=Use Wrapped Scrolling scroll_wrapped_label=Wrapped Scrolling +spread_none.title=Do not join page spreads +spread_none_label=No Spreads +spread_odd.title=Join page spreads starting with odd-numbered pages +spread_odd_label=Odd Spreads +spread_even.title=Join page spreads starting with even-numbered pages +spread_even_label=Even Spreads + # Document properties dialog box document_properties.title=Document Properties… document_properties_label=Document Properties… diff --git a/web/app.js b/web/app.js index 705c1a523..5891d1de7 100644 --- a/web/app.js +++ b/web/app.js @@ -1386,6 +1386,7 @@ let PDFViewerApplication = { eventBus.on('rotatecw', webViewerRotateCw); eventBus.on('rotateccw', webViewerRotateCcw); eventBus.on('switchscrollmode', webViewerSwitchScrollMode); + eventBus.on('switchspreadmode', webViewerSwitchSpreadMode); eventBus.on('documentproperties', webViewerDocumentProperties); eventBus.on('find', webViewerFind); eventBus.on('findfromurlhash', webViewerFindFromUrlHash); @@ -1453,6 +1454,7 @@ let PDFViewerApplication = { eventBus.off('rotatecw', webViewerRotateCw); eventBus.off('rotateccw', webViewerRotateCcw); eventBus.off('switchscrollmode', webViewerSwitchScrollMode); + eventBus.off('switchspreadmode', webViewerSwitchSpreadMode); eventBus.off('documentproperties', webViewerDocumentProperties); eventBus.off('find', webViewerFind); eventBus.off('findfromurlhash', webViewerFindFromUrlHash); @@ -1965,6 +1967,9 @@ function webViewerRotateCcw() { function webViewerSwitchScrollMode(evt) { PDFViewerApplication.pdfViewer.setScrollMode(evt.mode); } +function webViewerSwitchSpreadMode(evt) { + PDFViewerApplication.pdfViewer.setSpreadMode(evt.mode); +} function webViewerDocumentProperties() { PDFViewerApplication.pdfDocumentProperties.open(); } diff --git a/web/base_viewer.js b/web/base_viewer.js index 94de1779c..1c8d5bce4 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -35,6 +35,12 @@ const ScrollMode = { WRAPPED: 2, }; +const SpreadMode = { + NONE: 0, // The default value. + ODD: 1, + EVEN: 2, +}; + /** * @typedef {Object} PDFViewerOptions * @property {HTMLDivElement} container - The container for the viewer element. @@ -71,6 +77,10 @@ const ScrollMode = { * document pages should be laid out within the scrolling container. The * constants from {ScrollMode} should be used. The default value is * `ScrollMode.VERTICAL`. + * @property {number} spreadMode - (optional) If not `SpreadMode.NONE`, groups + * pages into spreads, starting with odd- or even-numbered pages. The + * constants from {SpreadMode} should be used. The default value is + * `SpreadMode.NONE`. */ function PDFPageViewBuffer(size) { @@ -153,6 +163,7 @@ class BaseViewer { this.maxCanvasPixels = options.maxCanvasPixels; this.l10n = options.l10n || NullL10n; this.scrollMode = options.scrollMode || ScrollMode.VERTICAL; + this.spreadMode = options.spreadMode || SpreadMode.NONE; this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { @@ -428,6 +439,9 @@ class BaseViewer { bindOnAfterAndBeforeDraw(pageView); this._pages.push(pageView); } + if (this.spreadMode !== SpreadMode.NONE) { + this._regroupSpreads(); + } // Fetch all the pages since the viewport is needed before printing // starts to create the correct size canvas. Wait until one page is @@ -1020,9 +1034,20 @@ class BaseViewer { classList.toggle('scrollHorizontal', mode === ScrollMode.HORIZONTAL); classList.toggle('scrollWrapped', mode === ScrollMode.WRAPPED); } + + setSpreadMode(mode) { + if (mode !== this.spreadMode) { + this.spreadMode = mode; + this.eventBus.dispatch('spreadmodechanged', { mode, }); + this._regroupSpreads(); + } + } + + _regroupSpreads() {} } export { BaseViewer, ScrollMode, + SpreadMode, }; diff --git a/web/images/secondaryToolbarButton-spreadEven.png b/web/images/secondaryToolbarButton-spreadEven.png new file mode 100644 index 0000000000000000000000000000000000000000..3fa07e703eaa56fa201db3e4ed6cde8bf849d608 GIT binary patch literal 347 zcmV-h0i^zkP)f*c{dlCL1q@VmAOItx{sSlR;WCL2patwCy#$P9 z!#;o~U>_)eWneV7(o#p%hB~JXrl}QwPUoUJqYhQPa_|N$0IR@xL>#N#%t23mO7{06 z;#BSB4j$ACwVCvW`uL4ssS`)8q~5z$bWfY($TBzu~7bp@DAwE>`)HK?lxz&J3i9#$X#I-SmCQ52p0lXSb?vMkG~ te!K)|Hk zJ!n)x6otRLCTJlEs31{61T7RHpkj!XC@2;diikhp52Uab1RFciK#J5BS_mo#f}oLf zMnokcVh}W95>yBX62x7QBud_~$lYZ!dGj`4Vc}jFc4y|C;m(;eb00LK5smoY!>WHI zJpy*+U=(A4VKY0t05s3v>9{;jbuA0|VFKd;K#4@F^O)J@D1osAJ{+s{&1?%mQvk?v zNL6B%qex1rdYA=?G$lv_zztyB`x}81z)_%7CxL0;Ht-mj1e%?H5;&4`EHw!~cI3$f z*@!DJRg^#md;+@6Y)aA&ckcFRaTO?#1piWktnD>>_BPdk-{4Q3*W423|m~tyV d$^SQ3`dEaa-jzTV1)wV!JYD@<);T3K0RT5nKwSU; literal 0 HcmV?d00001 diff --git a/web/images/secondaryToolbarButton-spreadNone@2x.png b/web/images/secondaryToolbarButton-spreadNone@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8e51cf3b7d6ebed644db207d4e99391898ff3952 GIT binary patch literal 261 zcmV+g0s8)lP)PM>6+3`IT=LQA_{vFT; zlDs+RCMl%Ic?WRmE#NVeKp_9*1#s5nJ+5#oSIC?~P!|Cd{-d=AS{%RvDO+ye>HrsR z-|WKH60jo7L;!p2fwet=%G{Tw*wq0ldjpnZXubz1j3TVY@SE&?&K6Brt9iEQ0j?;| z?hZ(6nN|*P>ry&s15--RLWKCuv3(r~AzWjOCrzyPK8?rbz|Y_X&cCXl5!ijiZAtWjM3s91u3Q6yMb5v`(3@v(c=@2 z@EUiUVsjkhV@>c1Yps>TB#)DeewI?Ulf1(jbk_un>O&pm0+;wwN_mrH4^OT;7~wO% z;V<@a&?J~u@iTnId;Dn~qzcxtf!|F#aF%4Ni61RyRG{zs@8j{fZyO$lq3gQt(JH=! q>2&&HHk*yxfXQUC{DoEgzr__9eVLMZ=Mj zyK59t6vlritY{&tt&m2HSSg}GQLwVM5G}OO%0?6mL5Zl9O+>`Mz*?*WwyAOs;RFrC0C#sW)b zcBTeemp|&bluV_|Lix~v_W?i|M=L&NHWfMWE`ty6M*q!hKR_n{lnJ3quuMcG7pXQ( z&5?1!2t)(0NXPmPB~Bl zpMVKTJAli;ZeS;H(hF}0tTm-Tz}-wCsima_OnUx9eZPPg9+kcBP}x)i%JgrUxK9;f z*gXT_wwb+=WWfEXqmpeHJCiezN*)%iNL&>ee{<50-W9uM(GJ~swoAZ&FpJ!8JG=#Rafw{VGs>A zv4Lw547cH8V$52!Kt_BsJ088+tIX1g*R$5e?O0P0vc)!RYULV@ilHcq)o9i$G5-a~ zu{8?hTTf9GD;sK{*Xzyp`~6*;`HbpzyG4ArRrxWE>7UUb)UPDB$k`IH00000NkvXX Hu0mjf-&h!* literal 0 HcmV?d00001 diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index a0adbde3f..28f9f79a7 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -46,29 +46,40 @@ border: none; } -.pdfViewer.scrollHorizontal, .pdfViewer.scrollWrapped { +.pdfViewer.scrollHorizontal, .pdfViewer.scrollWrapped, .spread { margin-left: 3.5px; margin-right: 3.5px; text-align: center; } -.pdfViewer.scrollHorizontal { +.pdfViewer.scrollHorizontal, .spread { white-space: nowrap; } -.pdfViewer.removePageBorders { +.pdfViewer.removePageBorders, +.pdfViewer.scrollHorizontal .spread, +.pdfViewer.scrollWrapped .spread { margin-left: 0; margin-right: 0; } +.spread .page, .pdfViewer.scrollHorizontal .page, -.pdfViewer.scrollWrapped .page { +.pdfViewer.scrollWrapped .page, +.pdfViewer.scrollHorizontal .spread, +.pdfViewer.scrollWrapped .spread { display: inline-block; - margin-left: -3.5px; - margin-right: -3.5px; vertical-align: middle; } +.spread .page, +.pdfViewer.scrollHorizontal .page, +.pdfViewer.scrollWrapped .page { + margin-left: -3.5px; + margin-right: -3.5px; +} + +.pdfViewer.removePageBorders .spread .page, .pdfViewer.removePageBorders.scrollHorizontal .page, .pdfViewer.removePageBorders.scrollWrapped .page { margin-left: 5px; @@ -99,7 +110,8 @@ margin-right: 0; } -.pdfPresentationMode .pdfViewer .page { +.pdfPresentationMode .pdfViewer .page, +.pdfPresentationMode .pdfViewer .spread { display: block; } diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index e6e520f5f..2a6c46ca8 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { BaseViewer, ScrollMode } from './base_viewer'; +import { BaseViewer, ScrollMode, SpreadMode } from './base_viewer'; import { getVisibleElements, scrollIntoView } from './ui_utils'; import { shadow } from 'pdfjs-lib'; @@ -23,8 +23,14 @@ class PDFViewer extends BaseViewer { } _scrollIntoView({ pageDiv, pageSpot = null, }) { - if (!pageSpot && this.scrollMode === ScrollMode.HORIZONTAL) { - pageSpot = { left: 0, top: 0, }; + if (!pageSpot) { + const left = pageDiv.offsetLeft + pageDiv.clientLeft; + const right = left + pageDiv.clientWidth; + const { scrollLeft, clientWidth, } = this.container; + if (this.scrollMode === ScrollMode.HORIZONTAL || + left < scrollLeft || right > scrollLeft + clientWidth) { + pageSpot = { left: 0, top: 0, }; + } } scrollIntoView(pageDiv, pageSpot); } @@ -80,6 +86,34 @@ class PDFViewer extends BaseViewer { location: this._location, }); } + + _regroupSpreads() { + const container = this._setDocumentViewerElement, pages = this._pages; + while (container.firstChild) { + container.firstChild.remove(); + } + if (this.spreadMode === SpreadMode.NONE) { + for (let i = 0, iMax = pages.length; i < iMax; ++i) { + container.appendChild(pages[i].div); + } + } else { + const parity = this.spreadMode - 1; + let spread = null; + for (let i = 0, iMax = pages.length; i < iMax; ++i) { + if (spread === null) { + spread = document.createElement('div'); + spread.className = 'spread'; + container.appendChild(spread); + } else if (i % 2 === parity) { + spread = spread.cloneNode(false); + container.appendChild(spread); + } + spread.appendChild(pages[i].div); + } + } + this.scrollPageIntoView({ pageNumber: this._currentPageNumber, }); + this.update(); + } } export { diff --git a/web/secondary_toolbar.js b/web/secondary_toolbar.js index a332f668b..a44be0642 100644 --- a/web/secondary_toolbar.js +++ b/web/secondary_toolbar.js @@ -13,9 +13,9 @@ * limitations under the License. */ +import { ScrollMode, SpreadMode } from './base_viewer'; import { CursorTool } from './pdf_cursor_tools'; import { SCROLLBAR_PADDING } from './ui_utils'; -import { ScrollMode } from './base_viewer'; /** * @typedef {Object} SecondaryToolbarOptions @@ -83,6 +83,12 @@ class SecondaryToolbar { eventDetails: { mode: ScrollMode.HORIZONTAL, }, close: true, }, { element: options.scrollWrappedButton, eventName: 'switchscrollmode', eventDetails: { mode: ScrollMode.WRAPPED, }, close: true, }, + { element: options.spreadNoneButton, eventName: 'switchspreadmode', + eventDetails: { mode: SpreadMode.NONE, }, close: true, }, + { element: options.spreadOddButton, eventName: 'switchspreadmode', + eventDetails: { mode: SpreadMode.ODD, }, close: true, }, + { element: options.spreadEvenButton, eventName: 'switchspreadmode', + eventDetails: { mode: SpreadMode.EVEN, }, close: true, }, { element: options.documentPropertiesButton, eventName: 'documentproperties', close: true, }, ]; @@ -102,10 +108,12 @@ class SecondaryToolbar { this.reset(); - // Bind the event listeners for click, cursor tool, and scroll mode actions. + // Bind the event listeners for click, cursor tool, and scroll/spread mode + // actions. this._bindClickListeners(); this._bindCursorToolsListener(options); this._bindScrollModeListener(options); + this._bindSpreadModeListener(options); // Bind the event listener for adjusting the 'max-height' of the toolbar. this.eventBus.on('resize', this._setMaxHeight.bind(this)); @@ -191,6 +199,17 @@ class SecondaryToolbar { }); } + _bindSpreadModeListener(buttons) { + this.eventBus.on('spreadmodechanged', function(evt) { + buttons.spreadNoneButton.classList.toggle('toggled', + evt.mode === SpreadMode.NONE); + buttons.spreadOddButton.classList.toggle('toggled', + evt.mode === SpreadMode.ODD); + buttons.spreadEvenButton.classList.toggle('toggled', + evt.mode === SpreadMode.EVEN); + }); + } + open() { if (this.opened) { return; diff --git a/web/ui_utils.js b/web/ui_utils.js index b38489341..455bec351 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -316,9 +316,10 @@ function getPageSizeInches({ view, userUnit, rotate, }) { */ function backtrackBeforeAllVisibleElements(index, views, top) { // binarySearchFirstItem's assumption is that the input is ordered, with only - // one index where the conditions flips from false to true: - // [false ..., true...]. With wrapped scrolling, it is possible to have - // [false ..., true, false, true ...]. + // one index where the conditions flips from false to true: [false ..., + // true...]. With vertical scrolling and spreads, it is possible to have + // [false ..., true, false, true ...]. With wrapped scrolling we can have a + // similar sequence, with many more mixed true and false in the middle. // // So there is no guarantee that the binary search yields the index of the // first visible element. It could have been any of the other visible elements @@ -451,10 +452,11 @@ function getVisibleElements(scrollEl, views, sortByVisibility = false, isElementBottomAfterViewTop); if (views.length > 0 && !horizontal) { - // In wrapped scrolling, with some page sizes, isElementBottomAfterViewTop - // doesn't satisfy the binary search condition: there can be pages with - // bottoms above the view top between pages with bottoms below. This - // function detects and corrects that error; see it for more comments. + // In wrapped scrolling (or vertical scrolling with spreads), with some page + // sizes, isElementBottomAfterViewTop doesn't satisfy the binary search + // condition: there can be pages with bottoms above the view top between + // pages with bottoms below. This function detects and corrects that error; + // see it for more comments. firstVisibleElementInd = backtrackBeforeAllVisibleElements(firstVisibleElementInd, views, top); } @@ -462,11 +464,11 @@ function getVisibleElements(scrollEl, views, sortByVisibility = false, // lastEdge acts as a cutoff for us to stop looping, because we know all // subsequent pages will be hidden. // - // When using wrapped scrolling, we can't simply stop the first time we reach - // a page below the bottom of the view; the tops of subsequent pages on the - // same row could still be visible. In horizontal scrolling, we don't have - // that issue, so we can stop as soon as we pass `right`, without needing the - // code below that handles the -1 case. + // When using wrapped scrolling or vertical scrolling with spreads, we can't + // simply stop the first time we reach a page below the bottom of the view; + // the tops of subsequent pages on the same row could still be visible. In + // horizontal scrolling, we don't have that issue, so we can stop as soon as + // we pass `right`, without needing the code below that handles the -1 case. let lastEdge = horizontal ? right : -1; for (let i = firstVisibleElementInd, ii = views.length; i < ii; i++) { diff --git a/web/viewer.css b/web/viewer.css index 838ba81a6..9c3a3cada 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -978,6 +978,18 @@ html[dir="rtl"] .secondaryToolbarButton > span { content: url(images/secondaryToolbarButton-scrollWrapped.png); } +.secondaryToolbarButton.spreadNone::before { + content: url(images/secondaryToolbarButton-spreadNone.png); +} + +.secondaryToolbarButton.spreadOdd::before { + content: url(images/secondaryToolbarButton-spreadOdd.png); +} + +.secondaryToolbarButton.spreadEven::before { + content: url(images/secondaryToolbarButton-spreadEven.png); +} + .secondaryToolbarButton.documentProperties::before { content: url(images/secondaryToolbarButton-documentProperties.png); } @@ -1713,6 +1725,18 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { content: url(images/secondaryToolbarButton-scrollWrapped@2x.png); } + .secondaryToolbarButton.spreadNone::before { + content: url(images/secondaryToolbarButton-spreadNone@2x.png); + } + + .secondaryToolbarButton.spreadOdd::before { + content: url(images/secondaryToolbarButton-spreadOdd@2x.png); + } + + .secondaryToolbarButton.spreadEven::before { + content: url(images/secondaryToolbarButton-spreadEven@2x.png); + } + .secondaryToolbarButton.documentProperties::before { content: url(images/secondaryToolbarButton-documentProperties@2x.png); } diff --git a/web/viewer.html b/web/viewer.html index e92592e1a..ba0ac8e49 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -180,7 +180,19 @@ See https://github.com/adobe-type-tools/cmap-resources
- + + + +
+ + diff --git a/web/viewer.js b/web/viewer.js index 8206b9008..42088f314 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -99,6 +99,9 @@ function getViewerConfiguration() { scrollVerticalButton: document.getElementById('scrollVertical'), scrollHorizontalButton: document.getElementById('scrollHorizontal'), scrollWrappedButton: document.getElementById('scrollWrapped'), + spreadNoneButton: document.getElementById('spreadNone'), + spreadOddButton: document.getElementById('spreadOdd'), + spreadEvenButton: document.getElementById('spreadEven'), documentPropertiesButton: document.getElementById('documentProperties'), }, fullscreen: { From eaf14e5d47a0ef7fcf5186d5babf1c73b48ec662 Mon Sep 17 00:00:00 2001 From: Ryan Hendrickson Date: Mon, 14 May 2018 23:10:32 -0400 Subject: [PATCH 4/6] Modify key events for horizontal scrolling Specifically, when there is no vertical scrollbar, let up, down, page up, and page down all trigger moving to the next or previous page. --- web/app.js | 64 ++++++++++++++++++++++++++++++---------------- web/base_viewer.js | 5 ++++ 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/web/app.js b/web/app.js index 5891d1de7..95860bd8b 100644 --- a/web/app.js +++ b/web/app.js @@ -2229,28 +2229,31 @@ function webViewerKeyDown(evt) { } if (cmd === 0) { // no control key pressed at all. + let turnPage = 0, turnOnlyIfPageFit = false; switch (evt.keyCode) { case 38: // up arrow case 33: // pg up - case 8: // backspace - if (!isViewerInPresentationMode && - pdfViewer.currentScaleValue !== 'page-fit') { - break; + // vertical scrolling using arrow/pg keys + if (pdfViewer.isVerticalScrollbarEnabled) { + turnOnlyIfPageFit = true; } - /* in presentation mode */ - /* falls through */ + turnPage = -1; + break; + case 8: // backspace + if (!isViewerInPresentationMode) { + turnOnlyIfPageFit = true; + } + turnPage = -1; + break; case 37: // left arrow // horizontal scrolling using arrow keys if (pdfViewer.isHorizontalScrollbarEnabled) { - break; + turnOnlyIfPageFit = true; } /* falls through */ case 75: // 'k' case 80: // 'p' - if (PDFViewerApplication.page > 1) { - PDFViewerApplication.page--; - } - handled = true; + turnPage = -1; break; case 27: // esc key if (PDFViewerApplication.secondaryToolbar.isOpen) { @@ -2263,27 +2266,30 @@ function webViewerKeyDown(evt) { handled = true; } break; - case 13: // enter key case 40: // down arrow case 34: // pg down - case 32: // spacebar - if (!isViewerInPresentationMode && - pdfViewer.currentScaleValue !== 'page-fit') { - break; + // vertical scrolling using arrow/pg keys + if (pdfViewer.isVerticalScrollbarEnabled) { + turnOnlyIfPageFit = true; } - /* falls through */ + turnPage = 1; + break; + case 13: // enter key + case 32: // spacebar + if (!isViewerInPresentationMode) { + turnOnlyIfPageFit = true; + } + turnPage = 1; + break; case 39: // right arrow // horizontal scrolling using arrow keys if (pdfViewer.isHorizontalScrollbarEnabled) { - break; + turnOnlyIfPageFit = true; } /* falls through */ case 74: // 'j' case 78: // 'n' - if (PDFViewerApplication.page < PDFViewerApplication.pagesCount) { - PDFViewerApplication.page++; - } - handled = true; + turnPage = 1; break; case 36: // home @@ -2313,6 +2319,20 @@ function webViewerKeyDown(evt) { PDFViewerApplication.rotatePages(90); break; } + + if (turnPage !== 0 && + (!turnOnlyIfPageFit || pdfViewer.currentScaleValue === 'page-fit')) { + if (turnPage > 0) { + if (PDFViewerApplication.page < PDFViewerApplication.pagesCount) { + PDFViewerApplication.page++; + } + } else { + if (PDFViewerApplication.page > 1) { + PDFViewerApplication.page--; + } + } + handled = true; + } } if (cmd === 4) { // shift-key diff --git a/web/base_viewer.js b/web/base_viewer.js index 1c8d5bce4..e084512b0 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -845,6 +845,11 @@ class BaseViewer { false : (this.container.scrollWidth > this.container.clientWidth)); } + get isVerticalScrollbarEnabled() { + return (this.isInPresentationMode ? + false : (this.container.scrollHeight > this.container.clientHeight)); + } + _getVisiblePages() { throw new Error('Not implemented: _getVisiblePages'); } From c24bc2975731fa69bf642bfb6f98408a366e39b6 Mon Sep 17 00:00:00 2001 From: Ryan Hendrickson Date: Mon, 14 May 2018 23:10:33 -0400 Subject: [PATCH 5/6] Remember last used scroll and spread modes --- web/app.js | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/web/app.js b/web/app.js index 95860bd8b..1f571bdc1 100644 --- a/web/app.js +++ b/web/app.js @@ -1029,6 +1029,8 @@ let PDFViewerApplication = { scrollTop: '0', rotation: null, sidebarView: SidebarView.NONE, + scrollMode: null, + spreadMode: null, }).catch(() => { /* Unable to read from storage; ignoring errors. */ }); Promise.all([storePromise, pageModePromise]).then( @@ -1038,6 +1040,8 @@ let PDFViewerApplication = { ('zoom=' + AppOptions.get('defaultZoomValue')) : null; let rotation = null; let sidebarView = AppOptions.get('sidebarViewOnLoad'); + let scrollMode = null; + let spreadMode = null; if (values.exists && AppOptions.get('showPreviousViewOnLoad')) { hash = 'page=' + values.page + @@ -1045,6 +1049,12 @@ let PDFViewerApplication = { ',' + values.scrollLeft + ',' + values.scrollTop; rotation = parseInt(values.rotation, 10); sidebarView = sidebarView || (values.sidebarView | 0); + if (values.scrollMode !== null) { + scrollMode = values.scrollMode; + } + if (values.spreadMode !== null) { + spreadMode = values.spreadMode; + } } if (pageMode && !AppOptions.get('disablePageMode')) { // Always let the user preference/history take precedence. @@ -1054,12 +1064,16 @@ let PDFViewerApplication = { hash, rotation, sidebarView, + scrollMode, + spreadMode, }; - }).then(({ hash, rotation, sidebarView, }) => { + }).then(({ hash, rotation, sidebarView, scrollMode, spreadMode, }) => { initialParams.bookmark = this.initialBookmark; initialParams.hash = hash; - this.setInitialView(hash, { rotation, sidebarView, }); + this.setInitialView(hash, { + rotation, sidebarView, scrollMode, spreadMode, + }); // Make all navigation keys work on document load, // unless the viewer is embedded in a web page. @@ -1227,12 +1241,20 @@ let PDFViewerApplication = { }); }, - setInitialView(storedHash, { rotation, sidebarView, } = {}) { + setInitialView(storedHash, values = {}) { + let { rotation, sidebarView, scrollMode, spreadMode, } = values; let setRotation = (angle) => { if (isValidRotation(angle)) { this.pdfViewer.pagesRotation = angle; } }; + if (Number.isInteger(scrollMode)) { + this.pdfViewer.setScrollMode(scrollMode); + } + if (Number.isInteger(spreadMode)) { + this.pdfViewer.setSpreadMode(spreadMode); + } + this.isInitialViewSet = true; this.pdfSidebar.setInitialView(sidebarView); @@ -1386,7 +1408,9 @@ let PDFViewerApplication = { eventBus.on('rotatecw', webViewerRotateCw); eventBus.on('rotateccw', webViewerRotateCcw); eventBus.on('switchscrollmode', webViewerSwitchScrollMode); + eventBus.on('scrollmodechanged', webViewerScrollModeChanged); eventBus.on('switchspreadmode', webViewerSwitchSpreadMode); + eventBus.on('spreadmodechanged', webViewerSpreadModeChanged); eventBus.on('documentproperties', webViewerDocumentProperties); eventBus.on('find', webViewerFind); eventBus.on('findfromurlhash', webViewerFindFromUrlHash); @@ -1454,7 +1478,9 @@ let PDFViewerApplication = { eventBus.off('rotatecw', webViewerRotateCw); eventBus.off('rotateccw', webViewerRotateCcw); eventBus.off('switchscrollmode', webViewerSwitchScrollMode); + eventBus.off('scrollmodechanged', webViewerScrollModeChanged); eventBus.off('switchspreadmode', webViewerSwitchSpreadMode); + eventBus.off('spreadmodechanged', webViewerSpreadModeChanged); eventBus.off('documentproperties', webViewerDocumentProperties); eventBus.off('find', webViewerFind); eventBus.off('findfromurlhash', webViewerFindFromUrlHash); @@ -1850,6 +1876,22 @@ function webViewerUpdateViewarea(evt) { PDFViewerApplication.toolbar.updateLoadingIndicatorState(loading); } +function webViewerScrollModeChanged(evt) { + let store = PDFViewerApplication.store; + if (store && PDFViewerApplication.isInitialViewSet) { + // Only update the storage when the document has been loaded *and* rendered. + store.set('scrollMode', evt.mode).catch(function() { }); + } +} + +function webViewerSpreadModeChanged(evt) { + let store = PDFViewerApplication.store; + if (store && PDFViewerApplication.isInitialViewSet) { + // Only update the storage when the document has been loaded *and* rendered. + store.set('spreadMode', evt.mode).catch(function() { }); + } +} + function webViewerResize() { let { pdfDocument, pdfViewer, } = PDFViewerApplication; if (!pdfDocument) { From d7c051e8070b036efbda0a955aec692e2135d97d Mon Sep 17 00:00:00 2001 From: Ryan Hendrickson Date: Mon, 14 May 2018 23:10:33 -0400 Subject: [PATCH 6/6] Add preferences for default scroll/spread modes This commit adds `scrollModeOnLoad` and `spreadModeOnLoad` preferences that control the default viewer state when opening a new document for the first time. This commit also contains a minor refactoring of some of the option UI rendering code in extensions/chromium/options/options.js, as I couldn't bear creating two more functions nearly identical to the four that already existed. --- extensions/chromium/options/options.html | 26 +++++ extensions/chromium/options/options.js | 100 +++++--------------- extensions/chromium/preferences_schema.json | 22 +++++ web/app.js | 14 ++- web/default_preferences.json | 4 +- 5 files changed, 87 insertions(+), 79 deletions(-) diff --git a/extensions/chromium/options/options.html b/extensions/chromium/options/options.html index 5b8569202..412e80bb1 100644 --- a/extensions/chromium/options/options.html +++ b/extensions/chromium/options/options.html @@ -120,6 +120,32 @@ body { + + + + diff --git a/extensions/chromium/options/options.js b/extensions/chromium/options/options.js index 2b5d005f5..cd4cf4086 100644 --- a/extensions/chromium/options/options.js +++ b/extensions/chromium/options/options.js @@ -75,16 +75,15 @@ Promise.all([ renderPreference = renderBooleanPref(prefSchema.title, prefSchema.description, prefName); + } else if (prefSchema.type === 'integer' && prefSchema.enum) { + // Most other prefs are integer-valued enumerations, render them in a + // generic way too. + // Unlike the renderBooleanPref branch, each preference handled by this + // branch still needs its own template in options.html with + // id="$prefName-template". + renderPreference = renderEnumPref(prefSchema.title, prefName); } else if (prefName === 'defaultZoomValue') { renderPreference = renderDefaultZoomValue(prefSchema.title); - } else if (prefName === 'sidebarViewOnLoad') { - renderPreference = renderSidebarViewOnLoad(prefSchema.title); - } else if (prefName === 'cursorToolOnLoad') { - renderPreference = renderCursorToolOnLoad(prefSchema.title); - } else if (prefName === 'textLayerMode') { - renderPreference = renderTextLayerMode(prefSchema.title); - } else if (prefName === 'externalLinkTarget') { - renderPreference = renderExternalLinkTarget(prefSchema.title); } else { // Should NEVER be reached. Only happens if a new type of preference is // added to the storage manifest. @@ -156,6 +155,23 @@ function renderBooleanPref(shortDescription, description, prefName) { return renderPreference; } +function renderEnumPref(shortDescription, prefName) { + var wrapper = importTemplate(prefName + '-template'); + var select = wrapper.querySelector('select'); + select.onchange = function() { + var pref = {}; + pref[prefName] = parseInt(this.value); + storageArea.set(pref); + }; + wrapper.querySelector('span').textContent = shortDescription; + document.getElementById('settings-boxes').appendChild(wrapper); + + function renderPreference(value) { + select.value = value; + } + return renderPreference; +} + function renderDefaultZoomValue(shortDescription) { var wrapper = importTemplate('defaultZoomValue-template'); var select = wrapper.querySelector('select'); @@ -184,71 +200,3 @@ function renderDefaultZoomValue(shortDescription) { } return renderPreference; } - -function renderSidebarViewOnLoad(shortDescription) { - var wrapper = importTemplate('sidebarViewOnLoad-template'); - var select = wrapper.querySelector('select'); - select.onchange = function() { - storageArea.set({ - sidebarViewOnLoad: parseInt(this.value), - }); - }; - wrapper.querySelector('span').textContent = shortDescription; - document.getElementById('settings-boxes').appendChild(wrapper); - - function renderPreference(value) { - select.value = value; - } - return renderPreference; -} - -function renderCursorToolOnLoad(shortDescription) { - var wrapper = importTemplate('cursorToolOnLoad-template'); - var select = wrapper.querySelector('select'); - select.onchange = function() { - storageArea.set({ - cursorToolOnLoad: parseInt(this.value), - }); - }; - wrapper.querySelector('span').textContent = shortDescription; - document.getElementById('settings-boxes').appendChild(wrapper); - - function renderPreference(value) { - select.value = value; - } - return renderPreference; -} - -function renderTextLayerMode(shortDescription) { - var wrapper = importTemplate('textLayerMode-template'); - var select = wrapper.querySelector('select'); - select.onchange = function() { - storageArea.set({ - textLayerMode: parseInt(this.value), - }); - }; - wrapper.querySelector('span').textContent = shortDescription; - document.getElementById('settings-boxes').appendChild(wrapper); - - function renderPreference(value) { - select.value = value; - } - return renderPreference; -} - -function renderExternalLinkTarget(shortDescription) { - var wrapper = importTemplate('externalLinkTarget-template'); - var select = wrapper.querySelector('select'); - select.onchange = function() { - storageArea.set({ - externalLinkTarget: parseInt(this.value), - }); - }; - wrapper.querySelector('span').textContent = shortDescription; - document.getElementById('settings-boxes').appendChild(wrapper); - - function renderPreference(value) { - select.value = value; - } - return renderPreference; -} diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 46d2d0c60..1833f61a6 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -144,6 +144,28 @@ "description": "When enabled, pages whose orientation differ from the first page are rotated when printed.", "type": "boolean", "default": false + }, + "scrollModeOnLoad": { + "title": "Scroll mode on load", + "description": "Controls how the viewer scrolls upon load.\n 0 = Vertical scrolling.\n 1 = Horizontal scrolling.\n 2 = Wrapped scrolling.", + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "default": 0 + }, + "spreadModeOnLoad": { + "title": "Spread mode on load", + "description": "Whether the viewer should join pages into spreads upon load.\n 0 = No spreads.\n 1 = Odd spreads.\n 2 = Even spreads.", + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "default": 0 } } } diff --git a/web/app.js b/web/app.js index 1f571bdc1..3aa891928 100644 --- a/web/app.js +++ b/web/app.js @@ -242,6 +242,12 @@ let PDFViewerApplication = { preferences.get('enablePrintAutoRotate').then(function resolved(value) { AppOptions.set('enablePrintAutoRotate', value); }), + preferences.get('scrollModeOnLoad').then(function resolved(value) { + AppOptions.set('scrollModeOnLoad', value); + }), + preferences.get('spreadModeOnLoad').then(function resolved(value) { + AppOptions.set('spreadModeOnLoad', value); + }), ]).catch(function(reason) { }); }, @@ -1040,8 +1046,8 @@ let PDFViewerApplication = { ('zoom=' + AppOptions.get('defaultZoomValue')) : null; let rotation = null; let sidebarView = AppOptions.get('sidebarViewOnLoad'); - let scrollMode = null; - let spreadMode = null; + let scrollMode = AppOptions.get('scrollModeOnLoad'); + let spreadMode = AppOptions.get('spreadModeOnLoad'); if (values.exists && AppOptions.get('showPreviousViewOnLoad')) { hash = 'page=' + values.page + @@ -1248,6 +1254,10 @@ let PDFViewerApplication = { this.pdfViewer.pagesRotation = angle; } }; + + // Putting these before isInitialViewSet = true prevents these values from + // being stored in the document history (and overriding any future changes + // made to the corresponding global preferences), just this once. if (Number.isInteger(scrollMode)) { this.pdfViewer.setScrollMode(scrollMode); } diff --git a/web/default_preferences.json b/web/default_preferences.json index 29e2515e8..78a998495 100644 --- a/web/default_preferences.json +++ b/web/default_preferences.json @@ -16,5 +16,7 @@ "renderInteractiveForms": false, "enablePrintAutoRotate": false, "disablePageMode": false, - "disablePageLabels": false + "disablePageLabels": false, + "scrollModeOnLoad": 0, + "spreadModeOnLoad": 0 }