Merge pull request #9208 from rhendric/scrolling-modes
Add scrolling and spread modes to web viewer
@ -120,6 +120,32 @@ body {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="scrollModeOnLoad-template">
|
||||||
|
<div class="settings-row">
|
||||||
|
<label>
|
||||||
|
<span></span>
|
||||||
|
<select>
|
||||||
|
<option value="0">Vertical scrolling</option>
|
||||||
|
<option value="1">Horizontal scrolling</option>
|
||||||
|
<option value="2">Wrapped scrolling</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="spreadModeOnLoad-template">
|
||||||
|
<div class="settings-row">
|
||||||
|
<label>
|
||||||
|
<span></span>
|
||||||
|
<select>
|
||||||
|
<option value="0">No spreads</option>
|
||||||
|
<option value="1">Odd spreads</option>
|
||||||
|
<option value="2">Even spreads</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script src="options.js"></script>
|
<script src="options.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -75,16 +75,15 @@ Promise.all([
|
|||||||
renderPreference = renderBooleanPref(prefSchema.title,
|
renderPreference = renderBooleanPref(prefSchema.title,
|
||||||
prefSchema.description,
|
prefSchema.description,
|
||||||
prefName);
|
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') {
|
} else if (prefName === 'defaultZoomValue') {
|
||||||
renderPreference = renderDefaultZoomValue(prefSchema.title);
|
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 {
|
} else {
|
||||||
// Should NEVER be reached. Only happens if a new type of preference is
|
// Should NEVER be reached. Only happens if a new type of preference is
|
||||||
// added to the storage manifest.
|
// added to the storage manifest.
|
||||||
@ -156,6 +155,23 @@ function renderBooleanPref(shortDescription, description, prefName) {
|
|||||||
return renderPreference;
|
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) {
|
function renderDefaultZoomValue(shortDescription) {
|
||||||
var wrapper = importTemplate('defaultZoomValue-template');
|
var wrapper = importTemplate('defaultZoomValue-template');
|
||||||
var select = wrapper.querySelector('select');
|
var select = wrapper.querySelector('select');
|
||||||
@ -184,71 +200,3 @@ function renderDefaultZoomValue(shortDescription) {
|
|||||||
}
|
}
|
||||||
return renderPreference;
|
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;
|
|
||||||
}
|
|
||||||
|
@ -144,6 +144,28 @@
|
|||||||
"description": "When enabled, pages whose orientation differ from the first page are rotated when printed.",
|
"description": "When enabled, pages whose orientation differ from the first page are rotated when printed.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,20 @@ cursor_text_select_tool_label=Text Selection Tool
|
|||||||
cursor_hand_tool.title=Enable Hand Tool
|
cursor_hand_tool.title=Enable Hand Tool
|
||||||
cursor_hand_tool_label=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
|
||||||
|
|
||||||
|
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 dialog box
|
||||||
document_properties.title=Document Properties…
|
document_properties.title=Document Properties…
|
||||||
document_properties_label=Document Properties…
|
document_properties_label=Document Properties…
|
||||||
|
@ -14,8 +14,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
binarySearchFirstItem, EventBus, getPageSizeInches, getPDFFileNameFromURL,
|
backtrackBeforeAllVisibleElements, binarySearchFirstItem, EventBus,
|
||||||
isPortraitOrientation, isValidRotation, waitOnEventOrTimeout, WaitOnType
|
getPageSizeInches, getPDFFileNameFromURL, getVisibleElements,
|
||||||
|
isPortraitOrientation, isValidRotation, moveToEndOfArray,
|
||||||
|
waitOnEventOrTimeout, WaitOnType
|
||||||
} from '../../web/ui_utils';
|
} from '../../web/ui_utils';
|
||||||
import { createObjectURL } from '../../src/shared/util';
|
import { createObjectURL } from '../../src/shared/util';
|
||||||
import isNodeJS from '../../src/shared/is_node';
|
import isNodeJS from '../../src/shared/is_node';
|
||||||
@ -447,4 +449,267 @@ describe('ui_utils', function() {
|
|||||||
expect(height2).toEqual(8.5);
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
132
web/app.js
@ -242,6 +242,12 @@ let PDFViewerApplication = {
|
|||||||
preferences.get('enablePrintAutoRotate').then(function resolved(value) {
|
preferences.get('enablePrintAutoRotate').then(function resolved(value) {
|
||||||
AppOptions.set('enablePrintAutoRotate', 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) { });
|
]).catch(function(reason) { });
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1029,6 +1035,8 @@ let PDFViewerApplication = {
|
|||||||
scrollTop: '0',
|
scrollTop: '0',
|
||||||
rotation: null,
|
rotation: null,
|
||||||
sidebarView: SidebarView.NONE,
|
sidebarView: SidebarView.NONE,
|
||||||
|
scrollMode: null,
|
||||||
|
spreadMode: null,
|
||||||
}).catch(() => { /* Unable to read from storage; ignoring errors. */ });
|
}).catch(() => { /* Unable to read from storage; ignoring errors. */ });
|
||||||
|
|
||||||
Promise.all([storePromise, pageModePromise]).then(
|
Promise.all([storePromise, pageModePromise]).then(
|
||||||
@ -1038,6 +1046,8 @@ let PDFViewerApplication = {
|
|||||||
('zoom=' + AppOptions.get('defaultZoomValue')) : null;
|
('zoom=' + AppOptions.get('defaultZoomValue')) : null;
|
||||||
let rotation = null;
|
let rotation = null;
|
||||||
let sidebarView = AppOptions.get('sidebarViewOnLoad');
|
let sidebarView = AppOptions.get('sidebarViewOnLoad');
|
||||||
|
let scrollMode = AppOptions.get('scrollModeOnLoad');
|
||||||
|
let spreadMode = AppOptions.get('spreadModeOnLoad');
|
||||||
|
|
||||||
if (values.exists && AppOptions.get('showPreviousViewOnLoad')) {
|
if (values.exists && AppOptions.get('showPreviousViewOnLoad')) {
|
||||||
hash = 'page=' + values.page +
|
hash = 'page=' + values.page +
|
||||||
@ -1045,6 +1055,12 @@ let PDFViewerApplication = {
|
|||||||
',' + values.scrollLeft + ',' + values.scrollTop;
|
',' + values.scrollLeft + ',' + values.scrollTop;
|
||||||
rotation = parseInt(values.rotation, 10);
|
rotation = parseInt(values.rotation, 10);
|
||||||
sidebarView = sidebarView || (values.sidebarView | 0);
|
sidebarView = sidebarView || (values.sidebarView | 0);
|
||||||
|
if (values.scrollMode !== null) {
|
||||||
|
scrollMode = values.scrollMode;
|
||||||
|
}
|
||||||
|
if (values.spreadMode !== null) {
|
||||||
|
spreadMode = values.spreadMode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pageMode && !AppOptions.get('disablePageMode')) {
|
if (pageMode && !AppOptions.get('disablePageMode')) {
|
||||||
// Always let the user preference/history take precedence.
|
// Always let the user preference/history take precedence.
|
||||||
@ -1054,12 +1070,16 @@ let PDFViewerApplication = {
|
|||||||
hash,
|
hash,
|
||||||
rotation,
|
rotation,
|
||||||
sidebarView,
|
sidebarView,
|
||||||
|
scrollMode,
|
||||||
|
spreadMode,
|
||||||
};
|
};
|
||||||
}).then(({ hash, rotation, sidebarView, }) => {
|
}).then(({ hash, rotation, sidebarView, scrollMode, spreadMode, }) => {
|
||||||
initialParams.bookmark = this.initialBookmark;
|
initialParams.bookmark = this.initialBookmark;
|
||||||
initialParams.hash = hash;
|
initialParams.hash = hash;
|
||||||
|
|
||||||
this.setInitialView(hash, { rotation, sidebarView, });
|
this.setInitialView(hash, {
|
||||||
|
rotation, sidebarView, scrollMode, spreadMode,
|
||||||
|
});
|
||||||
|
|
||||||
// Make all navigation keys work on document load,
|
// Make all navigation keys work on document load,
|
||||||
// unless the viewer is embedded in a web page.
|
// unless the viewer is embedded in a web page.
|
||||||
@ -1227,12 +1247,24 @@ let PDFViewerApplication = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setInitialView(storedHash, { rotation, sidebarView, } = {}) {
|
setInitialView(storedHash, values = {}) {
|
||||||
|
let { rotation, sidebarView, scrollMode, spreadMode, } = values;
|
||||||
let setRotation = (angle) => {
|
let setRotation = (angle) => {
|
||||||
if (isValidRotation(angle)) {
|
if (isValidRotation(angle)) {
|
||||||
this.pdfViewer.pagesRotation = angle;
|
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);
|
||||||
|
}
|
||||||
|
if (Number.isInteger(spreadMode)) {
|
||||||
|
this.pdfViewer.setSpreadMode(spreadMode);
|
||||||
|
}
|
||||||
|
|
||||||
this.isInitialViewSet = true;
|
this.isInitialViewSet = true;
|
||||||
this.pdfSidebar.setInitialView(sidebarView);
|
this.pdfSidebar.setInitialView(sidebarView);
|
||||||
|
|
||||||
@ -1385,6 +1417,10 @@ let PDFViewerApplication = {
|
|||||||
eventBus.on('scalechanged', webViewerScaleChanged);
|
eventBus.on('scalechanged', webViewerScaleChanged);
|
||||||
eventBus.on('rotatecw', webViewerRotateCw);
|
eventBus.on('rotatecw', webViewerRotateCw);
|
||||||
eventBus.on('rotateccw', webViewerRotateCcw);
|
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('documentproperties', webViewerDocumentProperties);
|
||||||
eventBus.on('find', webViewerFind);
|
eventBus.on('find', webViewerFind);
|
||||||
eventBus.on('findfromurlhash', webViewerFindFromUrlHash);
|
eventBus.on('findfromurlhash', webViewerFindFromUrlHash);
|
||||||
@ -1451,6 +1487,10 @@ let PDFViewerApplication = {
|
|||||||
eventBus.off('scalechanged', webViewerScaleChanged);
|
eventBus.off('scalechanged', webViewerScaleChanged);
|
||||||
eventBus.off('rotatecw', webViewerRotateCw);
|
eventBus.off('rotatecw', webViewerRotateCw);
|
||||||
eventBus.off('rotateccw', webViewerRotateCcw);
|
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('documentproperties', webViewerDocumentProperties);
|
||||||
eventBus.off('find', webViewerFind);
|
eventBus.off('find', webViewerFind);
|
||||||
eventBus.off('findfromurlhash', webViewerFindFromUrlHash);
|
eventBus.off('findfromurlhash', webViewerFindFromUrlHash);
|
||||||
@ -1846,6 +1886,22 @@ function webViewerUpdateViewarea(evt) {
|
|||||||
PDFViewerApplication.toolbar.updateLoadingIndicatorState(loading);
|
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() {
|
function webViewerResize() {
|
||||||
let { pdfDocument, pdfViewer, } = PDFViewerApplication;
|
let { pdfDocument, pdfViewer, } = PDFViewerApplication;
|
||||||
if (!pdfDocument) {
|
if (!pdfDocument) {
|
||||||
@ -1960,6 +2016,12 @@ function webViewerRotateCw() {
|
|||||||
function webViewerRotateCcw() {
|
function webViewerRotateCcw() {
|
||||||
PDFViewerApplication.rotatePages(-90);
|
PDFViewerApplication.rotatePages(-90);
|
||||||
}
|
}
|
||||||
|
function webViewerSwitchScrollMode(evt) {
|
||||||
|
PDFViewerApplication.pdfViewer.setScrollMode(evt.mode);
|
||||||
|
}
|
||||||
|
function webViewerSwitchSpreadMode(evt) {
|
||||||
|
PDFViewerApplication.pdfViewer.setSpreadMode(evt.mode);
|
||||||
|
}
|
||||||
function webViewerDocumentProperties() {
|
function webViewerDocumentProperties() {
|
||||||
PDFViewerApplication.pdfDocumentProperties.open();
|
PDFViewerApplication.pdfDocumentProperties.open();
|
||||||
}
|
}
|
||||||
@ -2219,28 +2281,31 @@ function webViewerKeyDown(evt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === 0) { // no control key pressed at all.
|
if (cmd === 0) { // no control key pressed at all.
|
||||||
|
let turnPage = 0, turnOnlyIfPageFit = false;
|
||||||
switch (evt.keyCode) {
|
switch (evt.keyCode) {
|
||||||
case 38: // up arrow
|
case 38: // up arrow
|
||||||
case 33: // pg up
|
case 33: // pg up
|
||||||
case 8: // backspace
|
// vertical scrolling using arrow/pg keys
|
||||||
if (!isViewerInPresentationMode &&
|
if (pdfViewer.isVerticalScrollbarEnabled) {
|
||||||
pdfViewer.currentScaleValue !== 'page-fit') {
|
turnOnlyIfPageFit = true;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
/* in presentation mode */
|
turnPage = -1;
|
||||||
/* falls through */
|
break;
|
||||||
|
case 8: // backspace
|
||||||
|
if (!isViewerInPresentationMode) {
|
||||||
|
turnOnlyIfPageFit = true;
|
||||||
|
}
|
||||||
|
turnPage = -1;
|
||||||
|
break;
|
||||||
case 37: // left arrow
|
case 37: // left arrow
|
||||||
// horizontal scrolling using arrow keys
|
// horizontal scrolling using arrow keys
|
||||||
if (pdfViewer.isHorizontalScrollbarEnabled) {
|
if (pdfViewer.isHorizontalScrollbarEnabled) {
|
||||||
break;
|
turnOnlyIfPageFit = true;
|
||||||
}
|
}
|
||||||
/* falls through */
|
/* falls through */
|
||||||
case 75: // 'k'
|
case 75: // 'k'
|
||||||
case 80: // 'p'
|
case 80: // 'p'
|
||||||
if (PDFViewerApplication.page > 1) {
|
turnPage = -1;
|
||||||
PDFViewerApplication.page--;
|
|
||||||
}
|
|
||||||
handled = true;
|
|
||||||
break;
|
break;
|
||||||
case 27: // esc key
|
case 27: // esc key
|
||||||
if (PDFViewerApplication.secondaryToolbar.isOpen) {
|
if (PDFViewerApplication.secondaryToolbar.isOpen) {
|
||||||
@ -2253,27 +2318,30 @@ function webViewerKeyDown(evt) {
|
|||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 13: // enter key
|
|
||||||
case 40: // down arrow
|
case 40: // down arrow
|
||||||
case 34: // pg down
|
case 34: // pg down
|
||||||
case 32: // spacebar
|
// vertical scrolling using arrow/pg keys
|
||||||
if (!isViewerInPresentationMode &&
|
if (pdfViewer.isVerticalScrollbarEnabled) {
|
||||||
pdfViewer.currentScaleValue !== 'page-fit') {
|
turnOnlyIfPageFit = true;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
/* falls through */
|
turnPage = 1;
|
||||||
|
break;
|
||||||
|
case 13: // enter key
|
||||||
|
case 32: // spacebar
|
||||||
|
if (!isViewerInPresentationMode) {
|
||||||
|
turnOnlyIfPageFit = true;
|
||||||
|
}
|
||||||
|
turnPage = 1;
|
||||||
|
break;
|
||||||
case 39: // right arrow
|
case 39: // right arrow
|
||||||
// horizontal scrolling using arrow keys
|
// horizontal scrolling using arrow keys
|
||||||
if (pdfViewer.isHorizontalScrollbarEnabled) {
|
if (pdfViewer.isHorizontalScrollbarEnabled) {
|
||||||
break;
|
turnOnlyIfPageFit = true;
|
||||||
}
|
}
|
||||||
/* falls through */
|
/* falls through */
|
||||||
case 74: // 'j'
|
case 74: // 'j'
|
||||||
case 78: // 'n'
|
case 78: // 'n'
|
||||||
if (PDFViewerApplication.page < PDFViewerApplication.pagesCount) {
|
turnPage = 1;
|
||||||
PDFViewerApplication.page++;
|
|
||||||
}
|
|
||||||
handled = true;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 36: // home
|
case 36: // home
|
||||||
@ -2303,6 +2371,20 @@ function webViewerKeyDown(evt) {
|
|||||||
PDFViewerApplication.rotatePages(90);
|
PDFViewerApplication.rotatePages(90);
|
||||||
break;
|
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
|
if (cmd === 4) { // shift-key
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, isPortraitOrientation,
|
CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, isPortraitOrientation,
|
||||||
isValidRotation, MAX_AUTO_SCALE, NullL10n, PresentationModeState,
|
isValidRotation, MAX_AUTO_SCALE, moveToEndOfArray, NullL10n,
|
||||||
RendererType, SCROLLBAR_PADDING, TextLayerMode, UNKNOWN_SCALE,
|
PresentationModeState, RendererType, SCROLLBAR_PADDING, TextLayerMode,
|
||||||
VERTICAL_PADDING, watchScroll
|
UNKNOWN_SCALE, VERTICAL_PADDING, watchScroll
|
||||||
} from './ui_utils';
|
} from './ui_utils';
|
||||||
import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue';
|
import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue';
|
||||||
import { AnnotationLayerBuilder } from './annotation_layer_builder';
|
import { AnnotationLayerBuilder } from './annotation_layer_builder';
|
||||||
@ -29,6 +29,18 @@ import { TextLayerBuilder } from './text_layer_builder';
|
|||||||
|
|
||||||
const DEFAULT_CACHE_SIZE = 10;
|
const DEFAULT_CACHE_SIZE = 10;
|
||||||
|
|
||||||
|
const ScrollMode = {
|
||||||
|
VERTICAL: 0, // The default value.
|
||||||
|
HORIZONTAL: 1,
|
||||||
|
WRAPPED: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SpreadMode = {
|
||||||
|
NONE: 0, // The default value.
|
||||||
|
ODD: 1,
|
||||||
|
EVEN: 2,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} PDFViewerOptions
|
* @typedef {Object} PDFViewerOptions
|
||||||
* @property {HTMLDivElement} container - The container for the viewer element.
|
* @property {HTMLDivElement} container - The container for the viewer element.
|
||||||
@ -61,6 +73,14 @@ const DEFAULT_CACHE_SIZE = 10;
|
|||||||
* size in total pixels, i.e. width * height. Use -1 for no limit.
|
* size in total pixels, i.e. width * height. Use -1 for no limit.
|
||||||
* The default value is 4096 * 4096 (16 mega-pixels).
|
* The default value is 4096 * 4096 (16 mega-pixels).
|
||||||
* @property {IL10n} l10n - Localization service.
|
* @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`.
|
||||||
|
* @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) {
|
function PDFPageViewBuffer(size) {
|
||||||
@ -75,8 +95,24 @@ function PDFPageViewBuffer(size) {
|
|||||||
data.shift().destroy();
|
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;
|
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) {
|
while (data.length > size) {
|
||||||
data.shift().destroy();
|
data.shift().destroy();
|
||||||
}
|
}
|
||||||
@ -126,6 +162,8 @@ class BaseViewer {
|
|||||||
this.useOnlyCssZoom = options.useOnlyCssZoom || false;
|
this.useOnlyCssZoom = options.useOnlyCssZoom || false;
|
||||||
this.maxCanvasPixels = options.maxCanvasPixels;
|
this.maxCanvasPixels = options.maxCanvasPixels;
|
||||||
this.l10n = options.l10n || NullL10n;
|
this.l10n = options.l10n || NullL10n;
|
||||||
|
this.scrollMode = options.scrollMode || ScrollMode.VERTICAL;
|
||||||
|
this.spreadMode = options.spreadMode || SpreadMode.NONE;
|
||||||
|
|
||||||
this.defaultRenderingQueue = !options.renderingQueue;
|
this.defaultRenderingQueue = !options.renderingQueue;
|
||||||
if (this.defaultRenderingQueue) {
|
if (this.defaultRenderingQueue) {
|
||||||
@ -143,6 +181,7 @@ class BaseViewer {
|
|||||||
if (this.removePageBorders) {
|
if (this.removePageBorders) {
|
||||||
this.viewer.classList.add('removePageBorders');
|
this.viewer.classList.add('removePageBorders');
|
||||||
}
|
}
|
||||||
|
this._updateScrollModeClasses();
|
||||||
}
|
}
|
||||||
|
|
||||||
get pagesCount() {
|
get pagesCount() {
|
||||||
@ -400,6 +439,9 @@ class BaseViewer {
|
|||||||
bindOnAfterAndBeforeDraw(pageView);
|
bindOnAfterAndBeforeDraw(pageView);
|
||||||
this._pages.push(pageView);
|
this._pages.push(pageView);
|
||||||
}
|
}
|
||||||
|
if (this.spreadMode !== SpreadMode.NONE) {
|
||||||
|
this._regroupSpreads();
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch all the pages since the viewport is needed before printing
|
// Fetch all the pages since the viewport is needed before printing
|
||||||
// starts to create the correct size canvas. Wait until one page is
|
// starts to create the correct size canvas. Wait until one page is
|
||||||
@ -557,6 +599,11 @@ class BaseViewer {
|
|||||||
0 : SCROLLBAR_PADDING;
|
0 : SCROLLBAR_PADDING;
|
||||||
let vPadding = (this.isInPresentationMode || this.removePageBorders) ?
|
let vPadding = (this.isInPresentationMode || this.removePageBorders) ?
|
||||||
0 : VERTICAL_PADDING;
|
0 : VERTICAL_PADDING;
|
||||||
|
if (this.scrollMode === ScrollMode.HORIZONTAL) {
|
||||||
|
const temp = hPadding;
|
||||||
|
hPadding = vPadding;
|
||||||
|
vPadding = temp;
|
||||||
|
}
|
||||||
let pageWidthScale = (this.container.clientWidth - hPadding) /
|
let pageWidthScale = (this.container.clientWidth - hPadding) /
|
||||||
currentPage.width * currentPage.scale;
|
currentPage.width * currentPage.scale;
|
||||||
let pageHeightScale = (this.container.clientHeight - vPadding) /
|
let pageHeightScale = (this.container.clientHeight - vPadding) /
|
||||||
@ -733,10 +780,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,
|
let suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE,
|
||||||
2 * numVisiblePages + 1);
|
2 * numVisiblePages + 1);
|
||||||
this._buffer.resize(suggestedCacheSize);
|
this._buffer.resize(suggestedCacheSize, visiblePages);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateLocation(firstPage) {
|
_updateLocation(firstPage) {
|
||||||
@ -793,6 +845,11 @@ class BaseViewer {
|
|||||||
false : (this.container.scrollWidth > this.container.clientWidth));
|
false : (this.container.scrollWidth > this.container.clientWidth));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isVerticalScrollbarEnabled() {
|
||||||
|
return (this.isInPresentationMode ?
|
||||||
|
false : (this.container.scrollHeight > this.container.clientHeight));
|
||||||
|
}
|
||||||
|
|
||||||
_getVisiblePages() {
|
_getVisiblePages() {
|
||||||
throw new Error('Not implemented: _getVisiblePages');
|
throw new Error('Not implemented: _getVisiblePages');
|
||||||
}
|
}
|
||||||
@ -847,9 +904,11 @@ class BaseViewer {
|
|||||||
|
|
||||||
forceRendering(currentlyVisiblePages) {
|
forceRendering(currentlyVisiblePages) {
|
||||||
let visiblePages = currentlyVisiblePages || this._getVisiblePages();
|
let visiblePages = currentlyVisiblePages || this._getVisiblePages();
|
||||||
|
let scrollAhead = this.scrollMode === ScrollMode.HORIZONTAL ?
|
||||||
|
this.scroll.right : this.scroll.down;
|
||||||
let pageView = this.renderingQueue.getHighestPriority(visiblePages,
|
let pageView = this.renderingQueue.getHighestPriority(visiblePages,
|
||||||
this._pages,
|
this._pages,
|
||||||
this.scroll.down);
|
scrollAhead);
|
||||||
if (pageView) {
|
if (pageView) {
|
||||||
this._ensurePdfPageLoaded(pageView).then(() => {
|
this._ensurePdfPageLoaded(pageView).then(() => {
|
||||||
this.renderingQueue.renderView(pageView);
|
this.renderingQueue.renderView(pageView);
|
||||||
@ -957,8 +1016,43 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpreadMode(mode) {
|
||||||
|
if (mode !== this.spreadMode) {
|
||||||
|
this.spreadMode = mode;
|
||||||
|
this.eventBus.dispatch('spreadmodechanged', { mode, });
|
||||||
|
this._regroupSpreads();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_regroupSpreads() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
BaseViewer,
|
BaseViewer,
|
||||||
|
ScrollMode,
|
||||||
|
SpreadMode,
|
||||||
};
|
};
|
||||||
|
@ -16,5 +16,7 @@
|
|||||||
"renderInteractiveForms": false,
|
"renderInteractiveForms": false,
|
||||||
"enablePrintAutoRotate": false,
|
"enablePrintAutoRotate": false,
|
||||||
"disablePageMode": false,
|
"disablePageMode": false,
|
||||||
"disablePageLabels": false
|
"disablePageLabels": false,
|
||||||
|
"scrollModeOnLoad": 0,
|
||||||
|
"spreadModeOnLoad": 0
|
||||||
}
|
}
|
||||||
|
BIN
web/images/secondaryToolbarButton-scrollHorizontal.png
Normal file
After Width: | Height: | Size: 218 B |
BIN
web/images/secondaryToolbarButton-scrollHorizontal@2x.png
Normal file
After Width: | Height: | Size: 332 B |
BIN
web/images/secondaryToolbarButton-scrollVertical.png
Normal file
After Width: | Height: | Size: 228 B |
BIN
web/images/secondaryToolbarButton-scrollVertical@2x.png
Normal file
After Width: | Height: | Size: 349 B |
BIN
web/images/secondaryToolbarButton-scrollWrapped.png
Normal file
After Width: | Height: | Size: 297 B |
BIN
web/images/secondaryToolbarButton-scrollWrapped@2x.png
Normal file
After Width: | Height: | Size: 490 B |
BIN
web/images/secondaryToolbarButton-spreadEven.png
Normal file
After Width: | Height: | Size: 347 B |
BIN
web/images/secondaryToolbarButton-spreadEven@2x.png
Normal file
After Width: | Height: | Size: 694 B |
BIN
web/images/secondaryToolbarButton-spreadNone.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
web/images/secondaryToolbarButton-spreadNone@2x.png
Normal file
After Width: | Height: | Size: 261 B |
BIN
web/images/secondaryToolbarButton-spreadOdd.png
Normal file
After Width: | Height: | Size: 344 B |
BIN
web/images/secondaryToolbarButton-spreadOdd@2x.png
Normal file
After Width: | Height: | Size: 621 B |
@ -46,6 +46,46 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdfViewer.scrollHorizontal, .pdfViewer.scrollWrapped, .spread {
|
||||||
|
margin-left: 3.5px;
|
||||||
|
margin-right: 3.5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdfViewer.scrollHorizontal, .spread {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdfViewer.removePageBorders,
|
||||||
|
.pdfViewer.scrollHorizontal .spread,
|
||||||
|
.pdfViewer.scrollWrapped .spread {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spread .page,
|
||||||
|
.pdfViewer.scrollHorizontal .page,
|
||||||
|
.pdfViewer.scrollWrapped .page,
|
||||||
|
.pdfViewer.scrollHorizontal .spread,
|
||||||
|
.pdfViewer.scrollWrapped .spread {
|
||||||
|
display: inline-block;
|
||||||
|
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;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.pdfViewer .page canvas {
|
.pdfViewer .page canvas {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: block;
|
display: block;
|
||||||
@ -65,6 +105,22 @@
|
|||||||
background: url('images/loading-icon.gif') center no-repeat;
|
background: url('images/loading-icon.gif') center no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdfPresentationMode .pdfViewer {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdfPresentationMode .pdfViewer .page,
|
||||||
|
.pdfPresentationMode .pdfViewer .spread {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdfPresentationMode .pdfViewer .page,
|
||||||
|
.pdfPresentationMode .pdfViewer.removePageBorders .page {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.pdfPresentationMode:-ms-fullscreen .pdfViewer .page {
|
.pdfPresentationMode:-ms-fullscreen .pdfViewer .page {
|
||||||
margin-bottom: 100% !important;
|
margin-bottom: 100% !important;
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { BaseViewer, ScrollMode, SpreadMode } from './base_viewer';
|
||||||
import { getVisibleElements, scrollIntoView } from './ui_utils';
|
import { getVisibleElements, scrollIntoView } from './ui_utils';
|
||||||
import { BaseViewer } from './base_viewer';
|
|
||||||
import { shadow } from 'pdfjs-lib';
|
import { shadow } from 'pdfjs-lib';
|
||||||
|
|
||||||
class PDFViewer extends BaseViewer {
|
class PDFViewer extends BaseViewer {
|
||||||
@ -23,12 +23,22 @@ class PDFViewer extends BaseViewer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_scrollIntoView({ pageDiv, pageSpot = null, }) {
|
_scrollIntoView({ pageDiv, pageSpot = null, }) {
|
||||||
|
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);
|
scrollIntoView(pageDiv, pageSpot);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getVisiblePages() {
|
_getVisiblePages() {
|
||||||
if (!this.isInPresentationMode) {
|
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
|
// The algorithm in getVisibleElements doesn't work in all browsers and
|
||||||
// configurations when presentation mode is active.
|
// configurations when presentation mode is active.
|
||||||
@ -44,7 +54,7 @@ class PDFViewer extends BaseViewer {
|
|||||||
if (numVisiblePages === 0) {
|
if (numVisiblePages === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._resizeBuffer(numVisiblePages);
|
this._resizeBuffer(numVisiblePages, visiblePages);
|
||||||
|
|
||||||
this.renderingQueue.renderHighestPriority(visible);
|
this.renderingQueue.renderHighestPriority(visible);
|
||||||
|
|
||||||
@ -76,6 +86,34 @@ class PDFViewer extends BaseViewer {
|
|||||||
location: this._location,
|
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 {
|
export {
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ScrollMode, SpreadMode } from './base_viewer';
|
||||||
import { CursorTool } from './pdf_cursor_tools';
|
import { CursorTool } from './pdf_cursor_tools';
|
||||||
import { SCROLLBAR_PADDING } from './ui_utils';
|
import { SCROLLBAR_PADDING } from './ui_utils';
|
||||||
|
|
||||||
@ -76,6 +77,18 @@ class SecondaryToolbar {
|
|||||||
eventDetails: { tool: CursorTool.SELECT, }, close: true, },
|
eventDetails: { tool: CursorTool.SELECT, }, close: true, },
|
||||||
{ element: options.cursorHandToolButton, eventName: 'switchcursortool',
|
{ element: options.cursorHandToolButton, eventName: 'switchcursortool',
|
||||||
eventDetails: { tool: CursorTool.HAND, }, close: true, },
|
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.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,
|
{ element: options.documentPropertiesButton,
|
||||||
eventName: 'documentproperties', close: true, },
|
eventName: 'documentproperties', close: true, },
|
||||||
];
|
];
|
||||||
@ -95,9 +108,12 @@ class SecondaryToolbar {
|
|||||||
|
|
||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
// Bind the event listeners for click and cursor tool actions.
|
// Bind the event listeners for click, cursor tool, and scroll/spread mode
|
||||||
|
// actions.
|
||||||
this._bindClickListeners();
|
this._bindClickListeners();
|
||||||
this._bindCursorToolsListener(options);
|
this._bindCursorToolsListener(options);
|
||||||
|
this._bindScrollModeListener(options);
|
||||||
|
this._bindSpreadModeListener(options);
|
||||||
|
|
||||||
// Bind the event listener for adjusting the 'max-height' of the toolbar.
|
// Bind the event listener for adjusting the 'max-height' of the toolbar.
|
||||||
this.eventBus.on('resize', this._setMaxHeight.bind(this));
|
this.eventBus.on('resize', this._setMaxHeight.bind(this));
|
||||||
@ -172,6 +188,28 @@ 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_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() {
|
open() {
|
||||||
if (this.opened) {
|
if (this.opened) {
|
||||||
return;
|
return;
|
||||||
|
222
web/ui_utils.js
@ -115,7 +115,8 @@ function scrollIntoView(element, spot, skipOverflowHiddenElements = false) {
|
|||||||
}
|
}
|
||||||
let offsetY = element.offsetTop + element.clientTop;
|
let offsetY = element.offsetTop + element.clientTop;
|
||||||
let offsetX = element.offsetLeft + element.clientLeft;
|
let offsetX = element.offsetLeft + element.clientLeft;
|
||||||
while (parent.clientHeight === parent.scrollHeight ||
|
while ((parent.clientHeight === parent.scrollHeight &&
|
||||||
|
parent.clientWidth === parent.scrollWidth) ||
|
||||||
(skipOverflowHiddenElements &&
|
(skipOverflowHiddenElements &&
|
||||||
getComputedStyle(parent).overflow === 'hidden')) {
|
getComputedStyle(parent).overflow === 'hidden')) {
|
||||||
if (parent.dataset._scaleY) {
|
if (parent.dataset._scaleY) {
|
||||||
@ -154,6 +155,12 @@ function watchScroll(viewAreaElement, callback) {
|
|||||||
rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
|
rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
|
||||||
rAF = null;
|
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 currentY = viewAreaElement.scrollTop;
|
||||||
let lastY = state.lastY;
|
let lastY = state.lastY;
|
||||||
if (currentY !== lastY) {
|
if (currentY !== lastY) {
|
||||||
@ -165,7 +172,9 @@ function watchScroll(viewAreaElement, callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let state = {
|
let state = {
|
||||||
|
right: true,
|
||||||
down: true,
|
down: true,
|
||||||
|
lastX: viewAreaElement.scrollLeft,
|
||||||
lastY: viewAreaElement.scrollTop,
|
lastY: viewAreaElement.scrollTop,
|
||||||
_eventHandler: debounceScroll,
|
_eventHandler: debounceScroll,
|
||||||
};
|
};
|
||||||
@ -295,50 +304,213 @@ 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 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
|
||||||
|
// 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 top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
|
||||||
let left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
|
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 element = view.div;
|
||||||
let elementBottom =
|
let elementBottom =
|
||||||
element.offsetTop + element.clientTop + element.clientHeight;
|
element.offsetTop + element.clientTop + element.clientHeight;
|
||||||
return elementBottom > top;
|
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 visible = [], view, element;
|
||||||
let currentHeight, viewHeight, hiddenHeight, percentHeight;
|
let currentHeight, viewHeight, viewBottom, hiddenHeight;
|
||||||
let currentWidth, viewWidth;
|
let currentWidth, viewWidth, viewRight, hiddenWidth;
|
||||||
|
let percentVisible;
|
||||||
let firstVisibleElementInd = views.length === 0 ? 0 :
|
let firstVisibleElementInd = views.length === 0 ? 0 :
|
||||||
binarySearchFirstItem(views, isElementBottomBelowViewTop);
|
binarySearchFirstItem(views, horizontal ? isElementRightAfterViewLeft :
|
||||||
|
isElementBottomAfterViewTop);
|
||||||
|
|
||||||
|
if (views.length > 0 && !horizontal) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastEdge acts as a cutoff for us to stop looping, because we know all
|
||||||
|
// subsequent pages will be hidden.
|
||||||
|
//
|
||||||
|
// 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++) {
|
for (let i = firstVisibleElementInd, ii = views.length; i < ii; i++) {
|
||||||
view = views[i];
|
view = views[i];
|
||||||
element = view.div;
|
element = view.div;
|
||||||
|
currentWidth = element.offsetLeft + element.clientLeft;
|
||||||
currentHeight = element.offsetTop + element.clientTop;
|
currentHeight = element.offsetTop + element.clientTop;
|
||||||
|
viewWidth = element.clientWidth;
|
||||||
viewHeight = element.clientHeight;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentWidth = element.offsetLeft + element.clientLeft;
|
if (viewBottom <= top || currentHeight >= bottom ||
|
||||||
viewWidth = element.clientWidth;
|
viewRight <= left || currentWidth >= right) {
|
||||||
if (currentWidth + viewWidth < left || currentWidth > right) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
hiddenHeight = Math.max(0, top - currentHeight) +
|
hiddenHeight = Math.max(0, top - currentHeight) +
|
||||||
Math.max(0, currentHeight + viewHeight - bottom);
|
Math.max(0, viewBottom - bottom);
|
||||||
percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0;
|
hiddenWidth = Math.max(0, left - currentWidth) +
|
||||||
|
Math.max(0, viewRight - right);
|
||||||
|
percentVisible = ((viewHeight - hiddenHeight) * (viewWidth - hiddenWidth) *
|
||||||
|
100 / viewHeight / viewWidth) | 0;
|
||||||
|
|
||||||
visible.push({
|
visible.push({
|
||||||
id: view.id,
|
id: view.id,
|
||||||
x: currentWidth,
|
x: currentWidth,
|
||||||
y: currentHeight,
|
y: currentHeight,
|
||||||
view,
|
view,
|
||||||
percent: percentHeight,
|
percent: percentVisible,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -639,6 +811,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 {
|
export {
|
||||||
CSS_UNITS,
|
CSS_UNITS,
|
||||||
DEFAULT_SCALE_VALUE,
|
DEFAULT_SCALE_VALUE,
|
||||||
@ -662,6 +854,7 @@ export {
|
|||||||
getPDFFileNameFromURL,
|
getPDFFileNameFromURL,
|
||||||
noContextMenuHandler,
|
noContextMenuHandler,
|
||||||
parseQueryString,
|
parseQueryString,
|
||||||
|
backtrackBeforeAllVisibleElements, // only exported for testing
|
||||||
getVisibleElements,
|
getVisibleElements,
|
||||||
roundToDivide,
|
roundToDivide,
|
||||||
getPageSizeInches,
|
getPageSizeInches,
|
||||||
@ -674,4 +867,5 @@ export {
|
|||||||
animationStarted,
|
animationStarted,
|
||||||
WaitOnType,
|
WaitOnType,
|
||||||
waitOnEventOrTimeout,
|
waitOnEventOrTimeout,
|
||||||
|
moveToEndOfArray,
|
||||||
};
|
};
|
||||||
|
@ -966,6 +966,30 @@ html[dir="rtl"] .secondaryToolbarButton > span {
|
|||||||
content: url(images/secondaryToolbarButton-handTool.png);
|
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.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 {
|
.secondaryToolbarButton.documentProperties::before {
|
||||||
content: url(images/secondaryToolbarButton-documentProperties.png);
|
content: url(images/secondaryToolbarButton-documentProperties.png);
|
||||||
}
|
}
|
||||||
@ -1689,6 +1713,30 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * {
|
|||||||
content: url(images/secondaryToolbarButton-handTool@2x.png);
|
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.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 {
|
.secondaryToolbarButton.documentProperties::before {
|
||||||
content: url(images/secondaryToolbarButton-documentProperties@2x.png);
|
content: url(images/secondaryToolbarButton-documentProperties@2x.png);
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,31 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||||||
|
|
||||||
<div class="horizontalToolbarSeparator"></div>
|
<div class="horizontalToolbarSeparator"></div>
|
||||||
|
|
||||||
<button id="documentProperties" class="secondaryToolbarButton documentProperties" title="Document Properties…" tabindex="62" data-l10n-id="document_properties">
|
<button id="scrollVertical" class="secondaryToolbarButton scrollVertical toggled" title="Use Vertical Scrolling" tabindex="62" data-l10n-id="scroll_vertical">
|
||||||
|
<span data-l10n-id="scroll_vertical_label">Vertical Scrolling</span>
|
||||||
|
</button>
|
||||||
|
<button id="scrollHorizontal" class="secondaryToolbarButton scrollHorizontal" title="Use Horizontal Scrolling" tabindex="63" data-l10n-id="scroll_horizontal">
|
||||||
|
<span data-l10n-id="scroll_horizontal_label">Horizontal Scrolling</span>
|
||||||
|
</button>
|
||||||
|
<button id="scrollWrapped" class="secondaryToolbarButton scrollWrapped" title="Use Wrapped Scrolling" tabindex="64" data-l10n-id="scroll_wrapped">
|
||||||
|
<span data-l10n-id="scroll_wrapped_label">Wrapped Scrolling</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator"></div>
|
||||||
|
|
||||||
|
<button id="spreadNone" class="secondaryToolbarButton spreadNone toggled" title="Do not join page spreads" tabindex="65" data-l10n-id="spread_none">
|
||||||
|
<span data-l10n-id="spread_none_label">No Spreads</span>
|
||||||
|
</button>
|
||||||
|
<button id="spreadOdd" class="secondaryToolbarButton spreadOdd" title="Join page spreads starting with odd-numbered pages" tabindex="66" data-l10n-id="spread_odd">
|
||||||
|
<span data-l10n-id="spread_odd_label">Odd Spreads</span>
|
||||||
|
</button>
|
||||||
|
<button id="spreadEven" class="secondaryToolbarButton spreadEven" title="Join page spreads starting with even-numbered pages" tabindex="67" data-l10n-id="spread_even">
|
||||||
|
<span data-l10n-id="spread_even_label">Even Spreads</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator"></div>
|
||||||
|
|
||||||
|
<button id="documentProperties" class="secondaryToolbarButton documentProperties" title="Document Properties…" tabindex="68" data-l10n-id="document_properties">
|
||||||
<span data-l10n-id="document_properties_label">Document Properties…</span>
|
<span data-l10n-id="document_properties_label">Document Properties…</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,6 +96,12 @@ function getViewerConfiguration() {
|
|||||||
pageRotateCcwButton: document.getElementById('pageRotateCcw'),
|
pageRotateCcwButton: document.getElementById('pageRotateCcw'),
|
||||||
cursorSelectToolButton: document.getElementById('cursorSelectTool'),
|
cursorSelectToolButton: document.getElementById('cursorSelectTool'),
|
||||||
cursorHandToolButton: document.getElementById('cursorHandTool'),
|
cursorHandToolButton: document.getElementById('cursorHandTool'),
|
||||||
|
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'),
|
documentPropertiesButton: document.getElementById('documentProperties'),
|
||||||
},
|
},
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
|