1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-21 15:48:06 +02:00

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.
This commit is contained in:
Ryan Hendrickson 2018-05-14 23:10:32 -04:00
parent 65c8549759
commit 91cbc185da
17 changed files with 665 additions and 27 deletions

View file

@ -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]);
});
});
});