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:
parent
65c8549759
commit
91cbc185da
17 changed files with 665 additions and 27 deletions
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue