blob: c27b902dcec8a9d60144d5db253e3da34733b8f0 [file] [log] [blame]
[email protected]3528d6302014-02-19 08:13:071// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
dpapad17f738f2019-10-22 21:03:025import {assert} from 'chrome://resources/js/assert.m.js';
6import {EventTracker} from 'chrome://resources/js/event_tracker.m.js';
rbpotter81f924a42020-06-11 23:00:037import {$, hasKeyModifiers} from 'chrome://resources/js/util.m.js';
dpapad17f738f2019-10-22 21:03:028
dpapad3c554222020-07-29 00:27:419import {FittingType, Point} from './constants.js';
dpapadb6fbeb22020-06-12 17:06:0910import {GestureDetector, PinchEventDetail} from './gesture_detector.js';
dpapad17f738f2019-10-22 21:03:0211import {InactiveZoomManager, ZoomManager} from './zoom_manager.js';
12
rbpotter84b2076a2019-08-23 01:31:1513/**
14 * @typedef {{
15 * width: number,
16 * height: number,
K Moonaa70882f2019-09-26 21:02:4917 * layoutOptions: (!LayoutOptions|undefined),
rbpotter84b2076a2019-08-23 01:31:1518 * pageDimensions: Array<ViewportRect>,
19 * }}
20 */
21let DocumentDimensions;
22
Hui Yingst73ba19a2020-03-10 19:45:2123/**
24 * @typedef {{
25 * defaultPageOrientation: number,
26 * twoUpViewEnabled: boolean,
27 * }}
28 */
dpapad17f738f2019-10-22 21:03:0229export let LayoutOptions;
K Moonaa70882f2019-09-26 21:02:4930
rbpotterde9429d2019-09-12 00:40:0831/** @typedef {{x: (number|undefined), y: (number|undefined)}} */
dpapad17f738f2019-10-22 21:03:0232export let PartialPoint;
rbpotterde9429d2019-09-12 00:40:0833
rbpotter84b2076a2019-08-23 01:31:1534/** @typedef {{width: number, height: number}} */
35let Size;
36
37/** @typedef {{x: number, y: number, width: number, height: number}} */
38let ViewportRect;
Henrique Nakashima585c7af02018-03-27 04:55:2139
40/**
rbpotter84b2076a2019-08-23 01:31:1541 * @param {!ViewportRect} rect1
42 * @param {!ViewportRect} rect2
43 * @return {number} The area of the intersection of the rects
[email protected]3528d6302014-02-19 08:13:0744 */
Jeremy Chinsen54f601822019-08-15 02:15:3745function getIntersectionArea(rect1, rect2) {
46 const left = Math.max(rect1.x, rect2.x);
47 const top = Math.max(rect1.y, rect2.y);
48 const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
49 const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
50
51 if (left >= right || top >= bottom) {
52 return 0;
53 }
54
55 return (right - left) * (bottom - top);
[email protected]3528d6302014-02-19 08:13:0756}
57
58/**
rbpotter84b2076a2019-08-23 01:31:1559 * @param {!Point} p1
60 * @param {!Point} p2
61 * @return {!Point} The vector between the two points.
mcnee6e1abbf2016-11-09 18:05:3162 */
63function vectorDelta(p1, p2) {
dbeam70db0fb2017-06-19 17:09:2764 return {x: p2.x - p1.x, y: p2.y - p1.y};
mcnee6e1abbf2016-11-09 18:05:3165}
66
dpapad17f738f2019-10-22 21:03:0267export class Viewport {
dstockwella685a702019-01-29 02:21:5468 /**
dpapada9c2e272020-07-27 00:49:2369 * @param {!HTMLElement} scrollParent
rbpotter84b2076a2019-08-23 01:31:1570 * @param {!HTMLDivElement} sizer The element which represents the size of the
dstockwella685a702019-01-29 02:21:5471 * document in the viewport
rbpottere94435742020-06-23 00:43:3272 * @param {!HTMLDivElement} content The element which is the parent of the
73 * plugin in the viewer.
rbpotter84b2076a2019-08-23 01:31:1574 * @param {number} scrollbarWidth The width of scrollbars on the page
dstockwella685a702019-01-29 02:21:5475 * @param {number} defaultZoom The default zoom level.
76 * @param {number} topToolbarHeight The number of pixels that should initially
77 * be left blank above the document for the toolbar.
rbpotter2a358912020-07-18 01:40:1178 * @param {boolean} topToolbarFixed True if the top toolbar is fixed and does
79 * not automatically disappear in fit to page mode.
dstockwella685a702019-01-29 02:21:5480 */
rbpotter1e76a072020-06-09 22:05:1481 constructor(
dpapada9c2e272020-07-27 00:49:2382 scrollParent, sizer, content, scrollbarWidth, defaultZoom,
83 topToolbarHeight, topToolbarFixed) {
84 /** @private {!HTMLElement} */
85 this.window_ = scrollParent;
rbpotter84b2076a2019-08-23 01:31:1586
87 /** @private {!HTMLDivElement} */
dstockwella685a702019-01-29 02:21:5488 this.sizer_ = sizer;
rbpotter84b2076a2019-08-23 01:31:1589
rbpottere94435742020-06-23 00:43:3290 /** @private {!HTMLDivElement} */
91 this.content_ = content;
92
rbpotter84b2076a2019-08-23 01:31:1593 /** @private {number} */
94 this.scrollbarWidth_ = scrollbarWidth;
95
96 /** @private {number} */
97 this.defaultZoom_ = defaultZoom;
98
99 /** @private {number} */
100 this.topToolbarHeight_ = topToolbarHeight;
101
rbpotter2a358912020-07-18 01:40:11102 /** @private {boolean} */
103 this.topToolbarFixed_ = topToolbarFixed;
104
rbpotter84b2076a2019-08-23 01:31:15105 /** @private {function():void} */
106 this.viewportChangedCallback_ = function() {};
107
108 /** @private {function():void} */
109 this.beforeZoomCallback_ = function() {};
110
111 /** @private {function():void} */
112 this.afterZoomCallback_ = function() {};
113
114 /** @private {function(boolean):void} */
115 this.userInitiatedCallback_ = function() {};
116
117 /** @private {boolean} */
dstockwella685a702019-01-29 02:21:54118 this.allowedToChangeZoom_ = false;
rbpotter84b2076a2019-08-23 01:31:15119
120 /** @private {number} */
dstockwella685a702019-01-29 02:21:54121 this.internalZoom_ = 1;
rbpotter84b2076a2019-08-23 01:31:15122
Brian Clifton08b57c02019-12-18 01:29:36123 /**
124 * Predefined zoom factors to be used when zooming in/out. These are in
125 * ascending order.
126 * @private {!Array<number>}
127 */
128 this.presetZoomFactors_ = [];
129
rbpotter84b2076a2019-08-23 01:31:15130 /** @private {?ZoomManager} */
131 this.zoomManager_ = null;
132
dstockwella685a702019-01-29 02:21:54133 /** @private {?DocumentDimensions} */
134 this.documentDimensions_ = null;
rbpotter84b2076a2019-08-23 01:31:15135
dstockwella685a702019-01-29 02:21:54136 /** @private {Array<ViewportRect>} */
137 this.pageDimensions_ = [];
rbpotter84b2076a2019-08-23 01:31:15138
139 /** @private {!FittingType} */
dstockwella685a702019-01-29 02:21:54140 this.fittingType_ = FittingType.NONE;
rbpotter84b2076a2019-08-23 01:31:15141
rbpotter84b2076a2019-08-23 01:31:15142 /** @private {number} */
dstockwella685a702019-01-29 02:21:54143 this.prevScale_ = 1;
rbpotter84b2076a2019-08-23 01:31:15144
dpapadd4ce7fd2020-09-22 07:25:06145 /** @private {!PinchPhase} */
146 this.pinchPhase_ = PinchPhase.PINCH_NONE;
rbpotter84b2076a2019-08-23 01:31:15147
148 /** @private {?Point} */
dstockwella685a702019-01-29 02:21:54149 this.pinchPanVector_ = null;
rbpotter84b2076a2019-08-23 01:31:15150
151 /** @private {?Point} */
dstockwella685a702019-01-29 02:21:54152 this.pinchCenter_ = null;
rbpotter84b2076a2019-08-23 01:31:15153
dstockwella685a702019-01-29 02:21:54154 /** @private {?Point} */
155 this.firstPinchCenterInFrame_ = null;
rbpotter84b2076a2019-08-23 01:31:15156
157 /** @private {number} */
dstockwella685a702019-01-29 02:21:54158 this.rotations_ = 0;
rbpotter84b2076a2019-08-23 01:31:15159
160 /** @private {?Point} */
161 this.oldCenterInContent_ = null;
162
163 /** @private {boolean} */
164 this.keepContentCentered_ = false;
165
166 /** @private {!EventTracker} */
167 this.tracker_ = new EventTracker();
168
rbpotter1e76a072020-06-09 22:05:14169 /** @private {!GestureDetector} */
rbpottere94435742020-06-23 00:43:32170 this.gestureDetector_ = new GestureDetector(this.content_);
rbpotter1e76a072020-06-09 22:05:14171
172 /** @private {boolean} */
173 this.sentPinchEvent_ = false;
174
dpapadb6fbeb22020-06-12 17:06:09175 this.gestureDetector_.getEventTarget().addEventListener(
176 'pinchstart',
177 e => this.onPinchStart_(
178 /** @type {!CustomEvent<!PinchEventDetail>} */ (e)));
179 this.gestureDetector_.getEventTarget().addEventListener(
180 'pinchupdate',
181 e => this.onPinchUpdate_(
182 /** @type {!CustomEvent<!PinchEventDetail>} */ (e)));
183 this.gestureDetector_.getEventTarget().addEventListener(
184 'pinchend',
185 e => this.onPinchEnd_(
186 /** @type {!CustomEvent<!PinchEventDetail>} */ (e)));
rbpotter1e76a072020-06-09 22:05:14187
rbpotter84b2076a2019-08-23 01:31:15188 // Set to a default zoom manager - used in tests.
189 this.setZoomManager(new InactiveZoomManager(this.getZoom.bind(this), 1));
[email protected]3528d6302014-02-19 08:13:07190
dpapad1b3e4012020-09-18 19:26:33191 // Case where |chrome_pdf::features::kPDFViewerUpdate| is disabled.
192 if (this.window_ === document.documentElement ||
193 // Necessary check since during testing a fake DOM element is used.
194 !(this.window_ instanceof HTMLElement)) {
dpapada9c2e272020-07-27 00:49:23195 window.addEventListener('scroll', this.updateViewport_.bind(this));
196 // The following line is only used in tests, since they expect
197 // |scrollCallback| to be called on the mock |window_| object (legacy).
198 this.window_.scrollCallback = this.updateViewport_.bind(this);
dpapad1b3e4012020-09-18 19:26:33199 window.addEventListener('resize', this.resizeWrapper_.bind(this));
200 // The following line is only used in tests, since they expect
201 // |resizeCallback| to be called on the mock |window_| object (legacy).
202 this.window_.resizeCallback = this.resizeWrapper_.bind(this);
dpapada9c2e272020-07-27 00:49:23203 } else {
dpapad1b3e4012020-09-18 19:26:33204 // Case where |chrome_pdf::features::kPDFViewerUpdate| is enabled.
dpapada9c2e272020-07-27 00:49:23205 this.window_.addEventListener('scroll', this.updateViewport_.bind(this));
dpapad1b3e4012020-09-18 19:26:33206 const resizeObserver = new ResizeObserver(_ => this.resizeWrapper_());
207 const target = this.window_.parentElement;
208 assert(target.id === 'main');
209 resizeObserver.observe(target);
dpapada9c2e272020-07-27 00:49:23210 }
211
rbpottere94435742020-06-23 00:43:32212 document.body.addEventListener(
213 'change-zoom', e => this.setZoom(e.detail.zoom));
dstockwella685a702019-01-29 02:21:54214 }
Douglas Stockwell1752f7762018-11-28 23:41:36215
rbpotter84b2076a2019-08-23 01:31:15216 /** @param {function():void} viewportChangedCallback */
217 setViewportChangedCallback(viewportChangedCallback) {
218 this.viewportChangedCallback_ = viewportChangedCallback;
219 }
220
221 /** @param {function():void} beforeZoomCallback */
222 setBeforeZoomCallback(beforeZoomCallback) {
223 this.beforeZoomCallback_ = beforeZoomCallback;
224 }
225
226 /** @param {function():void} afterZoomCallback */
227 setAfterZoomCallback(afterZoomCallback) {
228 this.afterZoomCallback_ = afterZoomCallback;
229 }
230
231 /** @param {function(boolean):void} userInitiatedCallback */
232 setUserInitiatedCallback(userInitiatedCallback) {
233 this.userInitiatedCallback_ = userInitiatedCallback;
234 }
235
Lei Zhangc6c4ec32019-10-31 21:14:28236 rotateClockwise() {
237 this.rotateBySteps_(1);
238 }
239
240 rotateCounterclockwise() {
241 this.rotateBySteps_(3);
242 }
243
Douglas Stockwell1752f7762018-11-28 23:41:36244 /**
rbpotter84b2076a2019-08-23 01:31:15245 * @param {number} n The number of clockwise 90-degree rotations to increment
246 * by.
Douglas Stockwell1752f7762018-11-28 23:41:36247 */
Lei Zhangc6c4ec32019-10-31 21:14:28248 rotateBySteps_(n) {
Douglas Stockwell1752f7762018-11-28 23:41:36249 this.rotations_ = (this.rotations_ + n) % 4;
dstockwella685a702019-01-29 02:21:54250 }
Douglas Stockwell1752f7762018-11-28 23:41:36251
252 /**
rbpotter84b2076a2019-08-23 01:31:15253 * @return {number} The number of clockwise 90-degree rotations that have been
Douglas Stockwell1752f7762018-11-28 23:41:36254 * applied.
255 */
dstockwella685a702019-01-29 02:21:54256 getClockwiseRotations() {
Douglas Stockwell1752f7762018-11-28 23:41:36257 return this.rotations_;
dstockwella685a702019-01-29 02:21:54258 }
Douglas Stockwell1752f7762018-11-28 23:41:36259
Hui Yingst73ba19a2020-03-10 19:45:21260 /** @return {boolean} Whether viewport is in two-up view mode. */
261 twoUpViewEnabled() {
262 const options = this.getLayoutOptions();
263 if (options === undefined) {
264 return false;
265 }
266 return options.twoUpViewEnabled;
Jeremy Chinsenfb6768e2019-08-17 01:09:07267 }
268
269 /**
Brian Clifton08b57c02019-12-18 01:29:36270 * Clamps the zoom factor (or page scale factor) to be within the limits.
271 * @param {number} factor The zoom/scale factor.
272 * @return {number} The factor clamped within the limits.
273 * @private
274 */
275 clampZoom_(factor) {
276 return Math.max(
277 this.presetZoomFactors_[0],
278 Math.min(
279 factor,
280 this.presetZoomFactors_[this.presetZoomFactors_.length - 1]));
281 }
282
283 /**
284 * @param {!Array<number>} factors Array containing zoom/scale factors.
285 */
286 setZoomFactorRange(factors) {
287 assert(factors.length !== 0);
288 this.presetZoomFactors_ = factors;
289 }
290
291 /**
Douglas Stockwell1752f7762018-11-28 23:41:36292 * Converts a page position (e.g. the location of a bookmark) to a screen
293 * position.
Douglas Stockwell1752f7762018-11-28 23:41:36294 * @param {number} page
rbpotter84b2076a2019-08-23 01:31:15295 * @param {!Point} point The position on `page`.
296 * @return {!Point} The screen position.
Douglas Stockwell1752f7762018-11-28 23:41:36297 */
dstockwella685a702019-01-29 02:21:54298 convertPageToScreen(page, point) {
Douglas Stockwell1752f7762018-11-28 23:41:36299 const dimensions = this.getPageInsetDimensions(page);
300
301 // width & height are already rotated.
302 const height = dimensions.height;
303 const width = dimensions.width;
304
305 const matrix = new DOMMatrix();
306
307 const rotation = this.rotations_ * 90;
308 // Set origin for rotation.
rbpotterb43063eb2020-02-20 01:23:10309 if (rotation === 90) {
Douglas Stockwell1752f7762018-11-28 23:41:36310 matrix.translateSelf(width, 0);
rbpotterb43063eb2020-02-20 01:23:10311 } else if (rotation === 180) {
Douglas Stockwell1752f7762018-11-28 23:41:36312 matrix.translateSelf(width, height);
rbpotterb43063eb2020-02-20 01:23:10313 } else if (rotation === 270) {
Douglas Stockwell1752f7762018-11-28 23:41:36314 matrix.translateSelf(0, height);
315 }
316 matrix.rotateSelf(0, 0, rotation);
317
318 // Invert Y position with respect to height as page coordinates are
319 // measured from the bottom left.
320 matrix.translateSelf(0, height);
321 matrix.scaleSelf(1, -1);
322
323 const pointsToPixels = 96 / 72;
324 const result = matrix.transformPoint({
325 x: point.x * pointsToPixels,
326 y: point.y * pointsToPixels,
327 });
328 return {
rbpotter81f924a42020-06-11 23:00:03329 x: result.x + PAGE_SHADOW.left,
330 y: result.y + PAGE_SHADOW.top,
Douglas Stockwell1752f7762018-11-28 23:41:36331 };
dstockwella685a702019-01-29 02:21:54332 }
Douglas Stockwell1752f7762018-11-28 23:41:36333
334
[email protected]3528d6302014-02-19 08:13:07335 /**
raymese6e90c62015-08-10 06:21:40336 * Returns the zoomed and rounded document dimensions for the given zoom.
337 * Rounding is necessary when interacting with the renderer which tends to
338 * operate in integral values (for example for determining if scrollbars
339 * should be shown).
340 * @param {number} zoom The zoom to use to compute the scaled dimensions.
rbpotter84b2076a2019-08-23 01:31:15341 * @return {?Size} Scaled 'width' and 'height' of the document.
raymese6e90c62015-08-10 06:21:40342 * @private
343 */
dstockwella685a702019-01-29 02:21:54344 getZoomedDocumentDimensions_(zoom) {
Dan Beamd1cca6e2019-01-03 02:46:27345 if (!this.documentDimensions_) {
raymese6e90c62015-08-10 06:21:40346 return null;
Dan Beamd1cca6e2019-01-03 02:46:27347 }
raymese6e90c62015-08-10 06:21:40348 return {
349 width: Math.round(this.documentDimensions_.width * zoom),
350 height: Math.round(this.documentDimensions_.height * zoom)
351 };
dstockwella685a702019-01-29 02:21:54352 }
raymese6e90c62015-08-10 06:21:40353
rbpotter84b2076a2019-08-23 01:31:15354 /** @return {!Size} A dictionary with the 'width'/'height' of the document. */
dstockwella685a702019-01-29 02:21:54355 getDocumentDimensions() {
dstockwell118c5362019-01-03 03:01:34356 return {
357 width: this.documentDimensions_.width,
358 height: this.documentDimensions_.height
359 };
dstockwella685a702019-01-29 02:21:54360 }
dstockwell118c5362019-01-03 03:01:34361
362 /**
K Moonaa70882f2019-09-26 21:02:49363 * @return {!LayoutOptions|undefined} A dictionary carrying layout options
364 * from the plugin.
365 */
366 getLayoutOptions() {
367 return this.documentDimensions_ ? this.documentDimensions_.layoutOptions :
368 undefined;
369 }
370
371 /**
rbpotter84b2076a2019-08-23 01:31:15372 * @return {!ViewportRect} ViewportRect for the viewport given current zoom.
Jeremy Chinsenfb6768e2019-08-17 01:09:07373 * @private
374 */
375 getViewportRect_() {
rbpotter84b2076a2019-08-23 01:31:15376 const zoom = this.getZoom();
Jeremy Chinsenfb6768e2019-08-17 01:09:07377 return {
rbpotter84b2076a2019-08-23 01:31:15378 x: this.position.x / zoom,
379 y: this.position.y / zoom,
380 width: this.size.width / zoom,
381 height: this.size.height / zoom
Jeremy Chinsenfb6768e2019-08-17 01:09:07382 };
383 }
384
385 /**
rbpotter84b2076a2019-08-23 01:31:15386 * @param {number} zoom Zoom to compute scrollbars for
387 * @return {{horizontal: boolean, vertical: boolean}} Whether horizontal or
dstockwella685a702019-01-29 02:21:54388 * vertical scrollbars are needed.
rbpotter81f924a42020-06-11 23:00:03389 * Public so tests can call it directly.
[email protected]3528d6302014-02-19 08:13:07390 */
rbpotter81f924a42020-06-11 23:00:03391 documentNeedsScrollbars(zoom) {
dstockwellafba4e42018-12-02 22:58:06392 const zoomedDimensions = this.getZoomedDocumentDimensions_(zoom);
raymese6e90c62015-08-10 06:21:40393 if (!zoomedDimensions) {
dbeam70db0fb2017-06-19 17:09:27394 return {horizontal: false, vertical: false};
[email protected]499e9562014-06-26 05:45:27395 }
sammc63334fed2014-11-10 03:07:35396
[email protected]3528d6302014-02-19 08:13:07397 return {
dpapada9c2e272020-07-27 00:49:23398 horizontal: zoomedDimensions.width > this.window_.offsetWidth,
raymes051fb2c2015-09-21 04:56:41399 vertical: zoomedDimensions.height + this.topToolbarHeight_ >
dpapada9c2e272020-07-27 00:49:23400 this.window_.offsetHeight
[email protected]3528d6302014-02-19 08:13:07401 };
dstockwella685a702019-01-29 02:21:54402 }
[email protected]3528d6302014-02-19 08:13:07403
404 /**
rbpotter84b2076a2019-08-23 01:31:15405 * @return {!{horizontal: boolean, vertical: boolean}} Whether horizontal and
406 * vertical scrollbars are needed.
[email protected]3528d6302014-02-19 08:13:07407 */
dstockwella685a702019-01-29 02:21:54408 documentHasScrollbars() {
rbpotter81f924a42020-06-11 23:00:03409 return this.documentNeedsScrollbars(this.getZoom());
dstockwella685a702019-01-29 02:21:54410 }
[email protected]3528d6302014-02-19 08:13:07411
412 /**
rbpotter84b2076a2019-08-23 01:31:15413 * Helper function called when the zoomed document size changes. Updates the
414 * sizer's width and height.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42415 * @private
[email protected]3528d6302014-02-19 08:13:07416 */
dstockwella685a702019-01-29 02:21:54417 contentSizeChanged_() {
rbpotter84b2076a2019-08-23 01:31:15418 const zoomedDimensions = this.getZoomedDocumentDimensions_(this.getZoom());
raymese6e90c62015-08-10 06:21:40419 if (zoomedDimensions) {
420 this.sizer_.style.width = zoomedDimensions.width + 'px';
dbeam70db0fb2017-06-19 17:09:27421 this.sizer_.style.height =
422 zoomedDimensions.height + this.topToolbarHeight_ + 'px';
[email protected]345e7c62014-05-02 09:52:58423 }
dstockwella685a702019-01-29 02:21:54424 }
[email protected]3528d6302014-02-19 08:13:07425
426 /**
rbpottere94435742020-06-23 00:43:32427 * @param {!Point} coordinateInFrame
428 * @return {!Point} Coordinate converted to plugin coordinates.
429 * @private
430 */
431 frameToPluginCoordinate_(coordinateInFrame) {
432 const container = this.content_.querySelector('#plugin');
433 return {
434 x: coordinateInFrame.x - container.getBoundingClientRect().left,
435 y: coordinateInFrame.y - container.getBoundingClientRect().top
436 };
437 }
438
439 /**
[email protected]312112c72014-04-14 01:45:43440 * Called when the viewport should be updated.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42441 * @private
[email protected]3528d6302014-02-19 08:13:07442 */
dstockwella685a702019-01-29 02:21:54443 updateViewport_() {
[email protected]312112c72014-04-14 01:45:43444 this.viewportChangedCallback_();
dstockwella685a702019-01-29 02:21:54445 }
[email protected]312112c72014-04-14 01:45:43446
447 /**
rbpottera82dacc2017-09-08 19:39:39448 * Called when the browser window size changes.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42449 * @private
rbpottera82dacc2017-09-08 19:39:39450 */
dstockwella685a702019-01-29 02:21:54451 resizeWrapper_() {
rbpotter84b2076a2019-08-23 01:31:15452 this.userInitiatedCallback_(false);
rbpottera82dacc2017-09-08 19:39:39453 this.resize_();
rbpotter84b2076a2019-08-23 01:31:15454 this.userInitiatedCallback_(true);
dstockwella685a702019-01-29 02:21:54455 }
rbpottera82dacc2017-09-08 19:39:39456
457 /**
[email protected]312112c72014-04-14 01:45:43458 * Called when the viewport size changes.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42459 * @private
[email protected]312112c72014-04-14 01:45:43460 */
dstockwella685a702019-01-29 02:21:54461 resize_() {
rbpotterb43063eb2020-02-20 01:23:10462 if (this.fittingType_ === FittingType.FIT_TO_PAGE) {
raymes161514f2015-02-17 05:09:51463 this.fitToPageInternal_(false);
rbpotterb43063eb2020-02-20 01:23:10464 } else if (this.fittingType_ === FittingType.FIT_TO_WIDTH) {
[email protected]312112c72014-04-14 01:45:43465 this.fitToWidth();
rbpotterb43063eb2020-02-20 01:23:10466 } else if (this.fittingType_ === FittingType.FIT_TO_HEIGHT) {
Henrique Nakashima50b18e02017-11-21 23:29:57467 this.fitToHeightInternal_(false);
rbpotterb43063eb2020-02-20 01:23:10468 } else if (this.internalZoom_ === 0) {
Henrique Nakashima4cf9ed42018-09-07 21:02:03469 this.fitToNone();
Dan Beamd1cca6e2019-01-03 02:46:27470 } else {
[email protected]312112c72014-04-14 01:45:43471 this.updateViewport_();
Dan Beamd1cca6e2019-01-03 02:46:27472 }
dstockwella685a702019-01-29 02:21:54473 }
[email protected]312112c72014-04-14 01:45:43474
rbpotter84b2076a2019-08-23 01:31:15475 /** @return {!Point} The scroll position of the viewport. */
[email protected]312112c72014-04-14 01:45:43476 get position() {
477 return {
dpapada9c2e272020-07-27 00:49:23478 x: this.window_.scrollLeft,
479 y: this.window_.scrollTop - this.topToolbarHeight_
[email protected]312112c72014-04-14 01:45:43480 };
dstockwella685a702019-01-29 02:21:54481 }
[email protected]312112c72014-04-14 01:45:43482
483 /**
484 * Scroll the viewport to the specified position.
rbpotter84b2076a2019-08-23 01:31:15485 * @param {!Point} position The position to scroll to.
[email protected]312112c72014-04-14 01:45:43486 */
487 set position(position) {
raymesbd82a942015-08-05 07:05:18488 this.window_.scrollTo(position.x, position.y + this.topToolbarHeight_);
dstockwella685a702019-01-29 02:21:54489 }
[email protected]312112c72014-04-14 01:45:43490
rbpotter84b2076a2019-08-23 01:31:15491 /** @return {!Size} the size of the viewport excluding scrollbars. */
[email protected]312112c72014-04-14 01:45:43492 get size() {
[email protected]312112c72014-04-14 01:45:43493 return {
dpapad45fed282020-08-04 09:54:06494 width: this.window_.offsetWidth,
495 height: this.window_.offsetHeight,
[email protected]312112c72014-04-14 01:45:43496 };
dstockwella685a702019-01-29 02:21:54497 }
[email protected]312112c72014-04-14 01:45:43498
rbpotter84b2076a2019-08-23 01:31:15499 /** @return {number} The current zoom. */
500 getZoom() {
mcnee2999413a2016-12-06 20:29:25501 return this.zoomManager_.applyBrowserZoom(this.internalZoom_);
dstockwella685a702019-01-29 02:21:54502 }
mcnee2999413a2016-12-06 20:29:25503
rbpotterf04e6e892020-10-13 18:51:24504 /** @return {!Array<number>} The preset zoom factors. */
505 get presetZoomFactors() {
506 return this.presetZoomFactors_;
507 }
508
rbpotter84b2076a2019-08-23 01:31:15509 /** @param {!ZoomManager} manager */
510 setZoomManager(manager) {
511 this.resetTracker();
mcnee2999413a2016-12-06 20:29:25512 this.zoomManager_ = manager;
rbpotter84b2076a2019-08-23 01:31:15513 this.tracker_.add(
514 this.zoomManager_.getEventTarget(), 'set-zoom',
515 e => this.setZoom(e.detail));
516 this.tracker_.add(
517 this.zoomManager_.getEventTarget(), 'update-zoom-from-browser',
518 this.updateZoomFromBrowserChange_.bind(this));
dstockwella685a702019-01-29 02:21:54519 }
[email protected]312112c72014-04-14 01:45:43520
521 /**
dpapadd4ce7fd2020-09-22 07:25:06522 * @return {!PinchPhase} The phase of the current pinch gesture for
mcnee6e1abbf2016-11-09 18:05:31523 * the viewport.
524 */
525 get pinchPhase() {
526 return this.pinchPhase_;
dstockwella685a702019-01-29 02:21:54527 }
mcnee6e1abbf2016-11-09 18:05:31528
529 /**
rbpotter84b2076a2019-08-23 01:31:15530 * @return {?Point} The panning caused by the current pinch gesture (as
mcnee6e1abbf2016-11-09 18:05:31531 * the deltas of the x and y coordinates).
532 */
533 get pinchPanVector() {
534 return this.pinchPanVector_;
dstockwella685a702019-01-29 02:21:54535 }
mcnee6e1abbf2016-11-09 18:05:31536
537 /**
rbpotter84b2076a2019-08-23 01:31:15538 * @return {?Point} The coordinates of the center of the current pinch
dstockwella685a702019-01-29 02:21:54539 * gesture.
mcnee6e1abbf2016-11-09 18:05:31540 */
541 get pinchCenter() {
542 return this.pinchCenter_;
dstockwella685a702019-01-29 02:21:54543 }
mcnee6e1abbf2016-11-09 18:05:31544
545 /**
[email protected]499e9562014-06-26 05:45:27546 * Used to wrap a function that might perform zooming on the viewport. This is
547 * required so that we can notify the plugin that zooming is in progress
548 * so that while zooming is taking place it can stop reacting to scroll events
549 * from the viewport. This is to avoid flickering.
rbpotter84b2076a2019-08-23 01:31:15550 * @param {function():void} f Function to wrap
Henrique Nakashimad5de6d0d2018-04-13 18:02:42551 * @private
[email protected]499e9562014-06-26 05:45:27552 */
dstockwella685a702019-01-29 02:21:54553 mightZoom_(f) {
[email protected]499e9562014-06-26 05:45:27554 this.beforeZoomCallback_();
555 this.allowedToChangeZoom_ = true;
556 f();
557 this.allowedToChangeZoom_ = false;
558 this.afterZoomCallback_();
rbpotter84b2076a2019-08-23 01:31:15559 this.zoomManager_.onPdfZoomChange();
dstockwella685a702019-01-29 02:21:54560 }
[email protected]499e9562014-06-26 05:45:27561
562 /**
rbpotter84b2076a2019-08-23 01:31:15563 * @param {number} newZoom The zoom level to set.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42564 * @private
[email protected]312112c72014-04-14 01:45:43565 */
dstockwella685a702019-01-29 02:21:54566 setZoomInternal_(newZoom) {
dstockwell154f9c42019-01-01 23:36:34567 assert(
568 this.allowedToChangeZoom_,
569 'Called Viewport.setZoomInternal_ without calling ' +
570 'Viewport.mightZoom_.');
sammc5c33b7e2014-11-07 04:03:09571 // Record the scroll position (relative to the top-left of the window).
rbpotter84b2076a2019-08-23 01:31:15572 let zoom = this.getZoom();
dstockwellafba4e42018-12-02 22:58:06573 const currentScrollPos = {
rbpotter84b2076a2019-08-23 01:31:15574 x: this.position.x / zoom,
575 y: this.position.y / zoom
raymesbd82a942015-08-05 07:05:18576 };
Henrique Nakashima4cf9ed42018-09-07 21:02:03577
mcnee2999413a2016-12-06 20:29:25578 this.internalZoom_ = newZoom;
[email protected]3528d6302014-02-19 08:13:07579 this.contentSizeChanged_();
580 // Scroll to the scaled scroll position.
rbpotter84b2076a2019-08-23 01:31:15581 zoom = this.getZoom();
raymesbd82a942015-08-05 07:05:18582 this.position = {
rbpotter84b2076a2019-08-23 01:31:15583 x: currentScrollPos.x * zoom,
584 y: currentScrollPos.y * zoom
raymesbd82a942015-08-05 07:05:18585 };
dstockwella685a702019-01-29 02:21:54586 }
[email protected]3528d6302014-02-19 08:13:07587
588 /**
mcnee6e1abbf2016-11-09 18:05:31589 * Sets the zoom of the viewport.
590 * Same as setZoomInternal_ but for pinch zoom we have some more operations.
591 * @param {number} scaleDelta The zoom delta.
rbpotter84b2076a2019-08-23 01:31:15592 * @param {!Point} center The pinch center in content coordinates.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42593 * @private
mcnee6e1abbf2016-11-09 18:05:31594 */
dstockwella685a702019-01-29 02:21:54595 setPinchZoomInternal_(scaleDelta, center) {
dbeam70db0fb2017-06-19 17:09:27596 assert(
597 this.allowedToChangeZoom_,
mcnee6e1abbf2016-11-09 18:05:31598 'Called Viewport.setPinchZoomInternal_ without calling ' +
dbeam70db0fb2017-06-19 17:09:27599 'Viewport.mightZoom_.');
Brian Clifton08b57c02019-12-18 01:29:36600 this.internalZoom_ = this.clampZoom_(this.internalZoom_ * scaleDelta);
mcnee6e1abbf2016-11-09 18:05:31601
rbpotter84b2076a2019-08-23 01:31:15602 const newCenterInContent = this.frameToContent_(center);
dstockwellafba4e42018-12-02 22:58:06603 const delta = {
rbpotter84b2076a2019-08-23 01:31:15604 x: (newCenterInContent.x - this.oldCenterInContent_.x),
605 y: (newCenterInContent.y - this.oldCenterInContent_.y)
mcnee6e1abbf2016-11-09 18:05:31606 };
607
608 // Record the scroll position (relative to the pinch center).
rbpotter84b2076a2019-08-23 01:31:15609 const zoom = this.getZoom();
dstockwellafba4e42018-12-02 22:58:06610 const currentScrollPos = {
rbpotter84b2076a2019-08-23 01:31:15611 x: this.position.x - delta.x * zoom,
612 y: this.position.y - delta.y * zoom
mcnee6e1abbf2016-11-09 18:05:31613 };
614
615 this.contentSizeChanged_();
616 // Scroll to the scaled scroll position.
dbeam70db0fb2017-06-19 17:09:27617 this.position = {x: currentScrollPos.x, y: currentScrollPos.y};
dstockwella685a702019-01-29 02:21:54618 }
mcnee6e1abbf2016-11-09 18:05:31619
620 /**
mcnee6e1abbf2016-11-09 18:05:31621 * Converts a point from frame to content coordinates.
rbpotter84b2076a2019-08-23 01:31:15622 * @param {!Point} framePoint The frame coordinates.
623 * @return {!Point} The content coordinates.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42624 * @private
mcnee6e1abbf2016-11-09 18:05:31625 */
rbpotter84b2076a2019-08-23 01:31:15626 frameToContent_(framePoint) {
mcnee6e1abbf2016-11-09 18:05:31627 // TODO(mcnee) Add a helper Point class to avoid duplicating operations
628 // on plain {x,y} objects.
rbpotter84b2076a2019-08-23 01:31:15629 const zoom = this.getZoom();
mcnee6e1abbf2016-11-09 18:05:31630 return {
rbpotter84b2076a2019-08-23 01:31:15631 x: (framePoint.x + this.position.x) / zoom,
632 y: (framePoint.y + this.position.y) / zoom
mcnee6e1abbf2016-11-09 18:05:31633 };
dstockwella685a702019-01-29 02:21:54634 }
mcnee6e1abbf2016-11-09 18:05:31635
rbpotter4b8484f2020-05-22 04:52:26636 /** @param {number} newZoom The zoom level to zoom to. */
dstockwella685a702019-01-29 02:21:54637 setZoom(newZoom) {
Henrique Nakashima50b18e02017-11-21 23:29:57638 this.fittingType_ = FittingType.NONE;
dpapad9afc2802017-08-09 22:01:43639 this.mightZoom_(() => {
Brian Clifton08b57c02019-12-18 01:29:36640 this.setZoomInternal_(this.clampZoom_(newZoom));
[email protected]fbad5bb2014-07-18 07:20:36641 this.updateViewport_();
dpapad9afc2802017-08-09 22:01:43642 });
dstockwella685a702019-01-29 02:21:54643 }
[email protected]499e9562014-06-26 05:45:27644
rbpotter84b2076a2019-08-23 01:31:15645 /**
646 * @param {!CustomEvent<number>} e Event containing the old browser zoom.
647 * @private
648 */
649 updateZoomFromBrowserChange_(e) {
650 const oldBrowserZoom = e.detail;
dpapad9afc2802017-08-09 22:01:43651 this.mightZoom_(() => {
mcnee2999413a2016-12-06 20:29:25652 // Record the scroll position (relative to the top-left of the window).
dstockwellafba4e42018-12-02 22:58:06653 const oldZoom = oldBrowserZoom * this.internalZoom_;
654 const currentScrollPos = {
mcnee2999413a2016-12-06 20:29:25655 x: this.position.x / oldZoom,
656 y: this.position.y / oldZoom
657 };
658 this.contentSizeChanged_();
rbpotter84b2076a2019-08-23 01:31:15659 const newZoom = this.getZoom();
mcnee2999413a2016-12-06 20:29:25660 // Scroll to the scaled scroll position.
661 this.position = {
rbpotter84b2076a2019-08-23 01:31:15662 x: currentScrollPos.x * newZoom,
663 y: currentScrollPos.y * newZoom
mcnee2999413a2016-12-06 20:29:25664 };
665 this.updateViewport_();
dpapad9afc2802017-08-09 22:01:43666 });
dstockwella685a702019-01-29 02:21:54667 }
mcnee2999413a2016-12-06 20:29:25668
rbpotter84b2076a2019-08-23 01:31:15669 /** @return {number} The width of scrollbars in the viewport in pixels. */
[email protected]312112c72014-04-14 01:45:43670 get scrollbarWidth() {
671 return this.scrollbarWidth_;
dstockwella685a702019-01-29 02:21:54672 }
[email protected]3528d6302014-02-19 08:13:07673
rbpotter84b2076a2019-08-23 01:31:15674 /** @return {FittingType} The fitting type the viewport is currently in. */
[email protected]312112c72014-04-14 01:45:43675 get fittingType() {
676 return this.fittingType_;
dstockwella685a702019-01-29 02:21:54677 }
[email protected]3528d6302014-02-19 08:13:07678
679 /**
Jeremy Chinsen54f601822019-08-15 02:15:37680 * Get the page at a given y position. If there are multiple pages
681 * overlapping the given y-coordinate, return the page with the smallest
682 * index.
rbpotter84b2076a2019-08-23 01:31:15683 * @param {number} y The y-coordinate to get the page at.
684 * @return {number} The index of a page overlapping the given y-coordinate.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42685 * @private
[email protected]56b7e3c2014-02-20 04:31:24686 */
dstockwella685a702019-01-29 02:21:54687 getPageAtY_(y) {
dstockwellafba4e42018-12-02 22:58:06688 let min = 0;
689 let max = this.pageDimensions_.length - 1;
[email protected]56b7e3c2014-02-20 04:31:24690 while (max >= min) {
dstockwellafba4e42018-12-02 22:58:06691 const page = Math.floor(min + ((max - min) / 2));
[email protected]13df2a42014-02-27 03:50:41692 // There might be a gap between the pages, in which case use the bottom
693 // of the previous page as the top for finding the page.
dstockwellafba4e42018-12-02 22:58:06694 let top = 0;
[email protected]13df2a42014-02-27 03:50:41695 if (page > 0) {
696 top = this.pageDimensions_[page - 1].y +
697 this.pageDimensions_[page - 1].height;
698 }
dstockwellafba4e42018-12-02 22:58:06699 const bottom =
dbeam70db0fb2017-06-19 17:09:27700 this.pageDimensions_[page].y + this.pageDimensions_[page].height;
[email protected]13df2a42014-02-27 03:50:41701
rbpotter0a1022f2019-10-10 18:13:50702 if (top <= y && y <= bottom) {
[email protected]56b7e3c2014-02-20 04:31:24703 return page;
Dan Beamd1cca6e2019-01-03 02:46:27704 }
Lei Zhangddc0ef032017-11-22 02:14:24705
Dan Beamd1cca6e2019-01-03 02:46:27706 if (top > y) {
[email protected]56b7e3c2014-02-20 04:31:24707 max = page - 1;
Dan Beamd1cca6e2019-01-03 02:46:27708 } else {
[email protected]56b7e3c2014-02-20 04:31:24709 min = page + 1;
Dan Beamd1cca6e2019-01-03 02:46:27710 }
[email protected]56b7e3c2014-02-20 04:31:24711 }
712 return 0;
dstockwella685a702019-01-29 02:21:54713 }
[email protected]56b7e3c2014-02-20 04:31:24714
Jeremy Chinsen54f601822019-08-15 02:15:37715 /**
Jeremy Chinsenfb6768e2019-08-17 01:09:07716 * Return the last page visible in the viewport. Returns the last index of the
717 * document if the viewport is below the document.
rbpotter84b2076a2019-08-23 01:31:15718 * @param {!ViewportRect} viewportRect
719 * @return {number} The highest index of the pages visible in the viewport.
Jeremy Chinsen54f601822019-08-15 02:15:37720 * @private
721 */
rbpotter84b2076a2019-08-23 01:31:15722 getLastPageInViewport_(viewportRect) {
Jeremy Chinsenfb6768e2019-08-17 01:09:07723 const pageAtY = this.getPageAtY_(viewportRect.y + viewportRect.height);
Jeremy Chinsen54f601822019-08-15 02:15:37724
Hui Yingst73ba19a2020-03-10 19:45:21725 if (!this.twoUpViewEnabled() || pageAtY % 2 === 1 ||
Jeremy Chinsenfb6768e2019-08-17 01:09:07726 pageAtY + 1 >= this.pageDimensions_.length) {
727 return pageAtY;
Jeremy Chinsen54f601822019-08-15 02:15:37728 }
729
Jeremy Chinsenfb6768e2019-08-17 01:09:07730 const nextPage = this.pageDimensions_[pageAtY + 1];
731 return getIntersectionArea(viewportRect, nextPage) > 0 ? pageAtY + 1 :
732 pageAtY;
Jeremy Chinsen54f601822019-08-15 02:15:37733 }
734
rbpotter84b2076a2019-08-23 01:31:15735 /**
736 * @param {!Point} point
737 * @return {boolean} Whether |point| (in screen coordinates) is inside a page
738 */
dstockwell09815ba2019-01-16 06:44:24739 isPointInsidePage(point) {
rbpotter84b2076a2019-08-23 01:31:15740 const zoom = this.getZoom();
dstockwell09815ba2019-01-16 06:44:24741 const size = this.size;
742 const position = this.position;
743 const page = this.getPageAtY_((position.y + point.y) / zoom);
744 const pageWidth = this.pageDimensions_[page].width * zoom;
745 const documentWidth = this.getDocumentDimensions().width * zoom;
746
747 const outerWidth = Math.max(size.width, documentWidth);
748
749 if (pageWidth >= outerWidth) {
750 return true;
751 }
752
753 const x = point.x + position.x;
754
755 const minX = (outerWidth - pageWidth) / 2;
756 const maxX = outerWidth - minX;
757 return x >= minX && x <= maxX;
dstockwella685a702019-01-29 02:21:54758 }
dstockwell09815ba2019-01-16 06:44:24759
760 /**
rbpotter84b2076a2019-08-23 01:31:15761 * @return {number} The index of the page with the greatest proportion of its
762 * area in the current viewport.
[email protected]3528d6302014-02-19 08:13:07763 */
dstockwella685a702019-01-29 02:21:54764 getMostVisiblePage() {
Jeremy Chinsenfb6768e2019-08-17 01:09:07765 const viewportRect = this.getViewportRect_();
Jeremy Chinsen54f601822019-08-15 02:15:37766
767 const firstVisiblePage = this.getPageAtY_(viewportRect.y);
rbpotter84b2076a2019-08-23 01:31:15768 const lastPossibleVisiblePage = this.getLastPageInViewport_(viewportRect);
Jeremy Chinsen54f601822019-08-15 02:15:37769 if (firstVisiblePage === lastPossibleVisiblePage) {
770 return firstVisiblePage;
Dan Beamd1cca6e2019-01-03 02:46:27771 }
Jeremy Chinsen54f601822019-08-15 02:15:37772
773 let mostVisiblePage = firstVisiblePage;
774 let largestIntersection = 0;
775
776 for (let i = firstVisiblePage; i < lastPossibleVisiblePage + 1; i++) {
777 const pageArea =
778 this.pageDimensions_[i].width * this.pageDimensions_[i].height;
779
780 // TODO(thestig): check whether we can remove this check.
781 if (pageArea <= 0) {
782 continue;
783 }
784
785 const pageIntersectionArea =
786 getIntersectionArea(this.pageDimensions_[i], viewportRect) / pageArea;
787
788 if (pageIntersectionArea > largestIntersection) {
789 mostVisiblePage = i;
790 largestIntersection = pageIntersectionArea;
791 }
792 }
793
794 return mostVisiblePage;
dstockwella685a702019-01-29 02:21:54795 }
[email protected]3528d6302014-02-19 08:13:07796
797 /**
Henrique Nakashima50b18e02017-11-21 23:29:57798 * Compute the zoom level for fit-to-page, fit-to-width or fit-to-height.
Henrique Nakashima50b18e02017-11-21 23:29:57799 * At least one of {fitWidth, fitHeight} must be true.
rbpotter84b2076a2019-08-23 01:31:15800 * @param {!Size} pageDimensions The dimensions of a given page in px.
801 * @param {boolean} fitWidth Whether the whole width of the page needs to be
802 * in the viewport.
803 * @param {boolean} fitHeight Whether the whole height of the page needs to be
804 * in the viewport.
805 * @return {number} The internal zoom to set
Henrique Nakashimad5de6d0d2018-04-13 18:02:42806 * @private
[email protected]3528d6302014-02-19 08:13:07807 */
dstockwella685a702019-01-29 02:21:54808 computeFittingZoom_(pageDimensions, fitWidth, fitHeight) {
Henrique Nakashima50b18e02017-11-21 23:29:57809 assert(
810 fitWidth || fitHeight,
811 'Invalid parameters. At least one of fitWidth and fitHeight must be ' +
812 'true.');
813
[email protected]3528d6302014-02-19 08:13:07814 // First compute the zoom without scrollbars.
dpapada9c2e272020-07-27 00:49:23815 let height = this.window_.offsetHeight;
rbpotter2a358912020-07-18 01:40:11816 if (this.topToolbarFixed_) {
817 height -= this.topToolbarHeight_;
818 }
dstockwellafba4e42018-12-02 22:58:06819 let zoom = this.computeFittingZoomGivenDimensions_(
dpapada9c2e272020-07-27 00:49:23820 fitWidth, fitHeight, this.window_.offsetWidth, height,
Henrique Nakashima50b18e02017-11-21 23:29:57821 pageDimensions.width, pageDimensions.height);
822
[email protected]3528d6302014-02-19 08:13:07823 // Check if there needs to be any scrollbars.
rbpotter81f924a42020-06-11 23:00:03824 const needsScrollbars = this.documentNeedsScrollbars(zoom);
[email protected]3528d6302014-02-19 08:13:07825
826 // If the document fits, just return the zoom.
Dan Beamd1cca6e2019-01-03 02:46:27827 if (!needsScrollbars.horizontal && !needsScrollbars.vertical) {
[email protected]3528d6302014-02-19 08:13:07828 return zoom;
Dan Beamd1cca6e2019-01-03 02:46:27829 }
[email protected]3528d6302014-02-19 08:13:07830
dstockwellafba4e42018-12-02 22:58:06831 const zoomedDimensions = this.getZoomedDocumentDimensions_(zoom);
[email protected]3528d6302014-02-19 08:13:07832
833 // Check if adding a scrollbar will result in needing the other scrollbar.
dstockwellafba4e42018-12-02 22:58:06834 const scrollbarWidth = this.scrollbarWidth_;
[email protected]312112c72014-04-14 01:45:43835 if (needsScrollbars.horizontal &&
dpapada9c2e272020-07-27 00:49:23836 zoomedDimensions.height > this.window_.offsetHeight - scrollbarWidth) {
[email protected]312112c72014-04-14 01:45:43837 needsScrollbars.vertical = true;
[email protected]3528d6302014-02-19 08:13:07838 }
[email protected]312112c72014-04-14 01:45:43839 if (needsScrollbars.vertical &&
dpapada9c2e272020-07-27 00:49:23840 zoomedDimensions.width > this.window_.offsetWidth - scrollbarWidth) {
[email protected]312112c72014-04-14 01:45:43841 needsScrollbars.horizontal = true;
[email protected]3528d6302014-02-19 08:13:07842 }
843
844 // Compute available window space.
dstockwellafba4e42018-12-02 22:58:06845 const windowWithScrollbars = {
dpapada9c2e272020-07-27 00:49:23846 width: this.window_.offsetWidth,
rbpotter2a358912020-07-18 01:40:11847 height: height,
[email protected]3528d6302014-02-19 08:13:07848 };
Dan Beamd1cca6e2019-01-03 02:46:27849 if (needsScrollbars.horizontal) {
[email protected]3528d6302014-02-19 08:13:07850 windowWithScrollbars.height -= scrollbarWidth;
Dan Beamd1cca6e2019-01-03 02:46:27851 }
852 if (needsScrollbars.vertical) {
[email protected]3528d6302014-02-19 08:13:07853 windowWithScrollbars.width -= scrollbarWidth;
Dan Beamd1cca6e2019-01-03 02:46:27854 }
[email protected]3528d6302014-02-19 08:13:07855
856 // Recompute the zoom.
Henrique Nakashima50b18e02017-11-21 23:29:57857 zoom = this.computeFittingZoomGivenDimensions_(
858 fitWidth, fitHeight, windowWithScrollbars.width,
859 windowWithScrollbars.height, pageDimensions.width,
860 pageDimensions.height);
861
mcnee2999413a2016-12-06 20:29:25862 return this.zoomManager_.internalZoomComponent(zoom);
dstockwella685a702019-01-29 02:21:54863 }
[email protected]3528d6302014-02-19 08:13:07864
865 /**
Henrique Nakashima50b18e02017-11-21 23:29:57866 * Compute a zoom level given the dimensions to fit and the actual numbers
867 * in those dimensions.
rbpotter84b2076a2019-08-23 01:31:15868 * @param {boolean} fitWidth Whether to constrain the page width to the
869 * window.
870 * @param {boolean} fitHeight Whether to constrain the page height to the
871 * window.
872 * @param {number} windowWidth Width of the window in px.
873 * @param {number} windowHeight Height of the window in px.
874 * @param {number} pageWidth Width of the page in px.
875 * @param {number} pageHeight Height of the page in px.
876 * @return {number} The internal zoom to set
Henrique Nakashimad5de6d0d2018-04-13 18:02:42877 * @private
Henrique Nakashima50b18e02017-11-21 23:29:57878 */
dstockwella685a702019-01-29 02:21:54879 computeFittingZoomGivenDimensions_(
Henrique Nakashima50b18e02017-11-21 23:29:57880 fitWidth, fitHeight, windowWidth, windowHeight, pageWidth, pageHeight) {
881 // Assumes at least one of {fitWidth, fitHeight} is set.
dstockwellafba4e42018-12-02 22:58:06882 let zoomWidth;
883 let zoomHeight;
Henrique Nakashima50b18e02017-11-21 23:29:57884
Dan Beamd1cca6e2019-01-03 02:46:27885 if (fitWidth) {
Henrique Nakashima50b18e02017-11-21 23:29:57886 zoomWidth = windowWidth / pageWidth;
Dan Beamd1cca6e2019-01-03 02:46:27887 }
Henrique Nakashima50b18e02017-11-21 23:29:57888
Dan Beamd1cca6e2019-01-03 02:46:27889 if (fitHeight) {
Henrique Nakashima50b18e02017-11-21 23:29:57890 zoomHeight = windowHeight / pageHeight;
Dan Beamd1cca6e2019-01-03 02:46:27891 }
Henrique Nakashima50b18e02017-11-21 23:29:57892
dstockwellafba4e42018-12-02 22:58:06893 let zoom;
Henrique Nakashima4cf9ed42018-09-07 21:02:03894 if (!fitWidth && fitHeight) {
895 zoom = zoomHeight;
896 } else if (fitWidth && !fitHeight) {
897 zoom = zoomWidth;
898 } else {
899 // Assume fitWidth && fitHeight
900 zoom = Math.min(zoomWidth, zoomHeight);
901 }
Henrique Nakashima50b18e02017-11-21 23:29:57902
Henrique Nakashima4cf9ed42018-09-07 21:02:03903 return Math.max(zoom, 0);
dstockwella685a702019-01-29 02:21:54904 }
Henrique Nakashima50b18e02017-11-21 23:29:57905
rbpotter84b2076a2019-08-23 01:31:15906 /** Zoom the viewport so that the page width consumes the entire viewport. */
dstockwella685a702019-01-29 02:21:54907 fitToWidth() {
dpapad9afc2802017-08-09 22:01:43908 this.mightZoom_(() => {
Henrique Nakashima50b18e02017-11-21 23:29:57909 this.fittingType_ = FittingType.FIT_TO_WIDTH;
Dan Beamd1cca6e2019-01-03 02:46:27910 if (!this.documentDimensions_) {
[email protected]499e9562014-06-26 05:45:27911 return;
Dan Beamd1cca6e2019-01-03 02:46:27912 }
[email protected]499e9562014-06-26 05:45:27913 // When computing fit-to-width, the maximum width of a page in the
914 // document is used, which is equal to the size of the document width.
dbeam70db0fb2017-06-19 17:09:27915 this.setZoomInternal_(
Henrique Nakashima50b18e02017-11-21 23:29:57916 this.computeFittingZoom_(this.documentDimensions_, true, false));
[email protected]499e9562014-06-26 05:45:27917 this.updateViewport_();
dpapad9afc2802017-08-09 22:01:43918 });
dstockwella685a702019-01-29 02:21:54919 }
[email protected]3528d6302014-02-19 08:13:07920
921 /**
Henrique Nakashima50b18e02017-11-21 23:29:57922 * Zoom the viewport so that the page height consumes the entire viewport.
923 * @param {boolean} scrollToTopOfPage Set to true if the viewport should be
924 * scrolled to the top of the current page. Set to false if the viewport
925 * should remain at the current scroll position.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42926 * @private
Henrique Nakashima50b18e02017-11-21 23:29:57927 */
dstockwella685a702019-01-29 02:21:54928 fitToHeightInternal_(scrollToTopOfPage) {
Henrique Nakashima50b18e02017-11-21 23:29:57929 this.mightZoom_(() => {
930 this.fittingType_ = FittingType.FIT_TO_HEIGHT;
Dan Beamd1cca6e2019-01-03 02:46:27931 if (!this.documentDimensions_) {
Henrique Nakashima50b18e02017-11-21 23:29:57932 return;
Dan Beamd1cca6e2019-01-03 02:46:27933 }
dstockwellafba4e42018-12-02 22:58:06934 const page = this.getMostVisiblePage();
Henrique Nakashima50b18e02017-11-21 23:29:57935 // When computing fit-to-height, the maximum height of the current page
936 // is used.
dstockwellafba4e42018-12-02 22:58:06937 const dimensions = {
Henrique Nakashima50b18e02017-11-21 23:29:57938 width: 0,
939 height: this.pageDimensions_[page].height,
940 };
941 this.setZoomInternal_(this.computeFittingZoom_(dimensions, false, true));
942 if (scrollToTopOfPage) {
rbpotter2a358912020-07-18 01:40:11943 const offset = this.topToolbarFixed_ ? this.topToolbarHeight_ : 0;
rbpotter84b2076a2019-08-23 01:31:15944 this.position = {
945 x: 0,
rbpotter2a358912020-07-18 01:40:11946 y: this.pageDimensions_[page].y * this.getZoom() - offset,
rbpotter84b2076a2019-08-23 01:31:15947 };
Henrique Nakashima50b18e02017-11-21 23:29:57948 }
949 this.updateViewport_();
950 });
dstockwella685a702019-01-29 02:21:54951 }
Henrique Nakashima50b18e02017-11-21 23:29:57952
rbpotter84b2076a2019-08-23 01:31:15953 /** Zoom the viewport so that the page height consumes the entire viewport. */
dstockwella685a702019-01-29 02:21:54954 fitToHeight() {
Henrique Nakashima50b18e02017-11-21 23:29:57955 this.fitToHeightInternal_(true);
dstockwella685a702019-01-29 02:21:54956 }
Henrique Nakashima50b18e02017-11-21 23:29:57957
958 /**
Henrique Nakashima50b18e02017-11-21 23:29:57959 * Zoom the viewport so that a page consumes as much as possible of the it.
rbpotter84b2076a2019-08-23 01:31:15960 * @param {boolean} scrollToTopOfPage Whether the viewport should be scrolled
961 * to the top of the current page. If false, the viewport will remain at
962 * the current scroll position.
Henrique Nakashimad5de6d0d2018-04-13 18:02:42963 * @private
[email protected]3528d6302014-02-19 08:13:07964 */
dstockwella685a702019-01-29 02:21:54965 fitToPageInternal_(scrollToTopOfPage) {
dpapad9afc2802017-08-09 22:01:43966 this.mightZoom_(() => {
Henrique Nakashima50b18e02017-11-21 23:29:57967 this.fittingType_ = FittingType.FIT_TO_PAGE;
Dan Beamd1cca6e2019-01-03 02:46:27968 if (!this.documentDimensions_) {
[email protected]499e9562014-06-26 05:45:27969 return;
Dan Beamd1cca6e2019-01-03 02:46:27970 }
dstockwellafba4e42018-12-02 22:58:06971 const page = this.getMostVisiblePage();
sammc07f53aa2014-11-06 06:57:46972 // Fit to the current page's height and the widest page's width.
dstockwellafba4e42018-12-02 22:58:06973 const dimensions = {
sammc07f53aa2014-11-06 06:57:46974 width: this.documentDimensions_.width,
975 height: this.pageDimensions_[page].height,
976 };
Henrique Nakashima50b18e02017-11-21 23:29:57977 this.setZoomInternal_(this.computeFittingZoom_(dimensions, true, true));
raymesbd82a942015-08-05 07:05:18978 if (scrollToTopOfPage) {
rbpotter2a358912020-07-18 01:40:11979 const offset = this.topToolbarFixed_ ? this.topToolbarHeight_ : 0;
rbpotter84b2076a2019-08-23 01:31:15980 this.position = {
981 x: 0,
rbpotter2a358912020-07-18 01:40:11982 y: this.pageDimensions_[page].y * this.getZoom() - offset,
rbpotter84b2076a2019-08-23 01:31:15983 };
raymesbd82a942015-08-05 07:05:18984 }
[email protected]499e9562014-06-26 05:45:27985 this.updateViewport_();
dpapad9afc2802017-08-09 22:01:43986 });
dstockwella685a702019-01-29 02:21:54987 }
[email protected]3528d6302014-02-19 08:13:07988
989 /**
raymes161514f2015-02-17 05:09:51990 * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
991 * the viewport to the top of the current page.
992 */
dstockwella685a702019-01-29 02:21:54993 fitToPage() {
raymes161514f2015-02-17 05:09:51994 this.fitToPageInternal_(true);
dstockwella685a702019-01-29 02:21:54995 }
raymes161514f2015-02-17 05:09:51996
rbpotter84b2076a2019-08-23 01:31:15997 /** Zoom the viewport to the default zoom. */
dstockwella685a702019-01-29 02:21:54998 fitToNone() {
Henrique Nakashima4cf9ed42018-09-07 21:02:03999 this.mightZoom_(() => {
1000 this.fittingType_ = FittingType.NONE;
Dan Beamd1cca6e2019-01-03 02:46:271001 if (!this.documentDimensions_) {
Henrique Nakashima4cf9ed42018-09-07 21:02:031002 return;
Dan Beamd1cca6e2019-01-03 02:46:271003 }
Henrique Nakashima4cf9ed42018-09-07 21:02:031004 this.setZoomInternal_(Math.min(
1005 this.defaultZoom_,
1006 this.computeFittingZoom_(this.documentDimensions_, true, false)));
1007 this.updateViewport_();
1008 });
dstockwella685a702019-01-29 02:21:541009 }
Henrique Nakashima4cf9ed42018-09-07 21:02:031010
rbpotter84b2076a2019-08-23 01:31:151011 /** Zoom out to the next predefined zoom level. */
dstockwella685a702019-01-29 02:21:541012 zoomOut() {
dpapad9afc2802017-08-09 22:01:431013 this.mightZoom_(() => {
Henrique Nakashima50b18e02017-11-21 23:29:571014 this.fittingType_ = FittingType.NONE;
Brian Clifton08b57c02019-12-18 01:29:361015 let nextZoom = this.presetZoomFactors_[0];
1016 for (let i = 0; i < this.presetZoomFactors_.length; i++) {
1017 if (this.presetZoomFactors_[i] < this.internalZoom_) {
1018 nextZoom = this.presetZoomFactors_[i];
Dan Beamd1cca6e2019-01-03 02:46:271019 }
[email protected]499e9562014-06-26 05:45:271020 }
[email protected]fbad5bb2014-07-18 07:20:361021 this.setZoomInternal_(nextZoom);
[email protected]499e9562014-06-26 05:45:271022 this.updateViewport_();
dpapad9afc2802017-08-09 22:01:431023 });
dstockwella685a702019-01-29 02:21:541024 }
[email protected]3528d6302014-02-19 08:13:071025
rbpotter84b2076a2019-08-23 01:31:151026 /** Zoom in to the next predefined zoom level. */
dstockwella685a702019-01-29 02:21:541027 zoomIn() {
dpapad9afc2802017-08-09 22:01:431028 this.mightZoom_(() => {
Henrique Nakashima50b18e02017-11-21 23:29:571029 this.fittingType_ = FittingType.NONE;
Brian Clifton08b57c02019-12-18 01:29:361030 const maxZoomIndex = this.presetZoomFactors_.length - 1;
1031 let nextZoom = this.presetZoomFactors_[maxZoomIndex];
1032 for (let i = maxZoomIndex; i >= 0; i--) {
1033 if (this.presetZoomFactors_[i] > this.internalZoom_) {
1034 nextZoom = this.presetZoomFactors_[i];
Dan Beamd1cca6e2019-01-03 02:46:271035 }
[email protected]499e9562014-06-26 05:45:271036 }
[email protected]fbad5bb2014-07-18 07:20:361037 this.setZoomInternal_(nextZoom);
[email protected]499e9562014-06-26 05:45:271038 this.updateViewport_();
dpapad9afc2802017-08-09 22:01:431039 });
dstockwella685a702019-01-29 02:21:541040 }
[email protected]3528d6302014-02-19 08:13:071041
1042 /**
rbpotter81f924a42020-06-11 23:00:031043 * @param {!KeyboardEvent} e
1044 * @private
1045 */
1046 pageUpHandler_(e) {
1047 // Go to the previous page if we are fit-to-page or fit-to-height.
1048 if (this.isPagedMode_()) {
1049 this.goToPreviousPage();
1050 // Since we do the movement of the page.
1051 e.preventDefault();
1052 } else if (
1053 /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e)
1054 .fromScriptingAPI) {
1055 this.position.y -= this.size.height;
1056 }
1057 }
1058
1059 /**
1060 * @param {!KeyboardEvent} e
1061 * @private
1062 */
1063 pageDownHandler_(e) {
1064 // Go to the next page if we are fit-to-page or fit-to-height.
1065 if (this.isPagedMode_()) {
1066 this.goToNextPage();
1067 // Since we do the movement of the page.
1068 e.preventDefault();
1069 } else if (
1070 /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e)
1071 .fromScriptingAPI) {
1072 this.position.y += this.size.height;
1073 }
1074 }
1075
1076 /**
1077 * @param {!KeyboardEvent} e
1078 * @param {boolean} formFieldFocused
1079 * @private
1080 */
1081 arrowLeftHandler_(e, formFieldFocused) {
1082 if (hasKeyModifiers(e)) {
1083 return;
1084 }
1085
1086 // Go to the previous page if there are no horizontal scrollbars and
1087 // no form field is focused.
1088 if (!(this.documentHasScrollbars().horizontal || formFieldFocused)) {
1089 this.goToPreviousPage();
1090 // Since we do the movement of the page.
1091 e.preventDefault();
1092 } else if (
1093 /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e)
1094 .fromScriptingAPI) {
1095 this.position.x -= SCROLL_INCREMENT;
1096 }
1097 }
1098
1099 /**
1100 * @param {!KeyboardEvent} e
1101 * @param {boolean} formFieldFocused
1102 * @private
1103 */
1104 arrowRightHandler_(e, formFieldFocused) {
1105 if (hasKeyModifiers(e)) {
1106 return;
1107 }
1108
1109 // Go to the next page if there are no horizontal scrollbars and no
1110 // form field is focused.
1111 if (!(this.documentHasScrollbars().horizontal || formFieldFocused)) {
1112 this.goToNextPage();
1113 // Since we do the movement of the page.
1114 e.preventDefault();
1115 } else if (
1116 /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e)
1117 .fromScriptingAPI) {
1118 this.position.x += SCROLL_INCREMENT;
1119 }
1120 }
1121
1122 /**
1123 * @param {boolean} fromScriptingAPI
1124 * @private
1125 */
1126 arrowDownHandler_(fromScriptingAPI) {
1127 if (fromScriptingAPI) {
1128 this.position.y += SCROLL_INCREMENT;
1129 }
1130 }
1131
1132 /**
1133 * @param {boolean} fromScriptingAPI
1134 * @private
1135 */
1136 arrowUpHandler_(fromScriptingAPI) {
1137 if (fromScriptingAPI) {
1138 this.position.y -= SCROLL_INCREMENT;
1139 }
1140 }
1141
1142 /**
1143 * Handle certain directional key events.
1144 * @param {!KeyboardEvent} e the event to handle.
1145 * @param {boolean} formFieldFocused Whether a form field is currently
1146 * focused.
1147 * @return {boolean} Whether the event was handled.
1148 */
1149 handleDirectionalKeyEvent(e, formFieldFocused) {
1150 // Certain scroll events may be sent from outside of the extension.
1151 const fromScriptingAPI =
1152 /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e)
1153 .fromScriptingAPI;
1154
1155 switch (e.key) {
1156 case '':
1157 if (e.shiftKey) {
1158 this.pageUpHandler_(e);
1159 } else {
1160 this.pageDownHandler_(e);
1161 }
1162 return true;
1163 case 'PageUp':
1164 this.pageUpHandler_(e);
1165 return true;
1166 case 'PageDown':
1167 this.pageDownHandler_(e);
1168 return true;
1169 case 'ArrowLeft':
1170 this.arrowLeftHandler_(e, formFieldFocused);
1171 return true;
1172 case 'ArrowUp':
1173 this.arrowUpHandler_(!!fromScriptingAPI);
1174 return true;
1175 case 'ArrowRight':
1176 this.arrowRightHandler_(e, formFieldFocused);
1177 return true;
1178 case 'ArrowDown':
1179 this.arrowDownHandler_(!!fromScriptingAPI);
1180 return true;
1181 default:
1182 return false;
1183 }
1184 }
1185
1186 /**
Jeremy Chinsenfb6768e2019-08-17 01:09:071187 * Go to the next page. If the document is in two-up view, go to the left page
rbpotter81f924a42020-06-11 23:00:031188 * of the next row. Public for tests.
Jeremy Chinsen168926a92019-08-16 16:22:151189 */
1190 goToNextPage() {
Jeremy Chinsenfb6768e2019-08-17 01:09:071191 const currentPage = this.getMostVisiblePage();
Hui Yingst73ba19a2020-03-10 19:45:211192 const nextPageOffset =
1193 (this.twoUpViewEnabled() && currentPage % 2 === 0) ? 2 : 1;
Jeremy Chinsenfb6768e2019-08-17 01:09:071194 this.goToPage(currentPage + nextPageOffset);
Jeremy Chinsen168926a92019-08-16 16:22:151195 }
1196
1197 /**
Jeremy Chinsenfb6768e2019-08-17 01:09:071198 * Go to the previous page. If the document is in two-up view, go to the left
rbpotter81f924a42020-06-11 23:00:031199 * page of the previous row. Public for tests.
Jeremy Chinsen168926a92019-08-16 16:22:151200 */
1201 goToPreviousPage() {
Jeremy Chinsenfb6768e2019-08-17 01:09:071202 const currentPage = this.getMostVisiblePage();
1203 let previousPageOffset = -1;
1204
Hui Yingst73ba19a2020-03-10 19:45:211205 if (this.twoUpViewEnabled()) {
rbpotterb43063eb2020-02-20 01:23:101206 previousPageOffset = (currentPage % 2 === 0) ? -2 : -3;
Jeremy Chinsenfb6768e2019-08-17 01:09:071207 }
1208
1209 this.goToPage(currentPage + previousPageOffset);
Jeremy Chinsen168926a92019-08-16 16:22:151210 }
1211
1212 /**
[email protected]3528d6302014-02-19 08:13:071213 * Go to the given page index.
[email protected]f90b9a42014-08-20 05:37:341214 * @param {number} page the index of the page to go to. zero-based.
[email protected]3528d6302014-02-19 08:13:071215 */
dstockwella685a702019-01-29 02:21:541216 goToPage(page) {
Henrique Nakashima85fc58b32017-12-11 21:53:421217 this.goToPageAndXY(page, 0, 0);
dstockwella685a702019-01-29 02:21:541218 }
Henrique Nakashima9d9e0632017-10-06 21:38:181219
1220 /**
1221 * Go to the given y position in the given page index.
1222 * @param {number} page the index of the page to go to. zero-based.
Henrique Nakashima85fc58b32017-12-11 21:53:421223 * @param {number} x the x position in the page to go to.
Henrique Nakashima9d9e0632017-10-06 21:38:181224 * @param {number} y the y position in the page to go to.
1225 */
dstockwella685a702019-01-29 02:21:541226 goToPageAndXY(page, x, y) {
dpapad9afc2802017-08-09 22:01:431227 this.mightZoom_(() => {
Dan Beamd1cca6e2019-01-03 02:46:271228 if (this.pageDimensions_.length === 0) {
[email protected]499e9562014-06-26 05:45:271229 return;
Dan Beamd1cca6e2019-01-03 02:46:271230 }
1231 if (page < 0) {
[email protected]499e9562014-06-26 05:45:271232 page = 0;
Dan Beamd1cca6e2019-01-03 02:46:271233 }
1234 if (page >= this.pageDimensions_.length) {
[email protected]499e9562014-06-26 05:45:271235 page = this.pageDimensions_.length - 1;
Dan Beamd1cca6e2019-01-03 02:46:271236 }
dstockwellafba4e42018-12-02 22:58:061237 const dimensions = this.pageDimensions_[page];
1238 let toolbarOffset = 0;
Henrique Nakashima50b18e02017-11-21 23:29:571239 // Unless we're in fit to page or fit to height mode, scroll above the
1240 // page by |this.topToolbarHeight_| so that the toolbar isn't covering it
raymesbd82a942015-08-05 07:05:181241 // initially.
rbpotter2a358912020-07-18 01:40:111242 if (!this.isPagedMode_() || this.topToolbarFixed_) {
raymesbd82a942015-08-05 07:05:181243 toolbarOffset = this.topToolbarHeight_;
Dan Beamd1cca6e2019-01-03 02:46:271244 }
raymesbd82a942015-08-05 07:05:181245 this.position = {
rbpotter84b2076a2019-08-23 01:31:151246 x: (dimensions.x + x) * this.getZoom(),
1247 y: (dimensions.y + y) * this.getZoom() - toolbarOffset
raymesbd82a942015-08-05 07:05:181248 };
[email protected]499e9562014-06-26 05:45:271249 this.updateViewport_();
dpapad9afc2802017-08-09 22:01:431250 });
dstockwella685a702019-01-29 02:21:541251 }
[email protected]3528d6302014-02-19 08:13:071252
1253 /**
rbpotter84b2076a2019-08-23 01:31:151254 * @param {DocumentDimensions} documentDimensions The dimensions of the
dstockwella685a702019-01-29 02:21:541255 * document
[email protected]3528d6302014-02-19 08:13:071256 */
dstockwella685a702019-01-29 02:21:541257 setDocumentDimensions(documentDimensions) {
dpapad9afc2802017-08-09 22:01:431258 this.mightZoom_(() => {
dstockwellafba4e42018-12-02 22:58:061259 const initialDimensions = !this.documentDimensions_;
[email protected]499e9562014-06-26 05:45:271260 this.documentDimensions_ = documentDimensions;
1261 this.pageDimensions_ = this.documentDimensions_.pageDimensions;
1262 if (initialDimensions) {
dbeam70db0fb2017-06-19 17:09:271263 this.setZoomInternal_(Math.min(
1264 this.defaultZoom_,
Henrique Nakashima50b18e02017-11-21 23:29:571265 this.computeFittingZoom_(this.documentDimensions_, true, false)));
dbeam70db0fb2017-06-19 17:09:271266 this.position = {x: 0, y: -this.topToolbarHeight_};
[email protected]499e9562014-06-26 05:45:271267 }
1268 this.contentSizeChanged_();
1269 this.resize_();
dpapad9afc2802017-08-09 22:01:431270 });
dstockwella685a702019-01-29 02:21:541271 }
[email protected]312112c72014-04-14 01:45:431272
1273 /**
Douglas Stockwell1752f7762018-11-28 23:41:361274 * @param {number} page
dstockwella685a702019-01-29 02:21:541275 * @return {ViewportRect} The bounds for page `page` minus the shadows.
Douglas Stockwell1752f7762018-11-28 23:41:361276 */
dstockwella685a702019-01-29 02:21:541277 getPageInsetDimensions(page) {
Douglas Stockwell1752f7762018-11-28 23:41:361278 const pageDimensions = this.pageDimensions_[page];
rbpotter81f924a42020-06-11 23:00:031279 const shadow = PAGE_SHADOW;
Douglas Stockwell1752f7762018-11-28 23:41:361280 return {
1281 x: pageDimensions.x + shadow.left,
1282 y: pageDimensions.y + shadow.top,
1283 width: pageDimensions.width - shadow.left - shadow.right,
1284 height: pageDimensions.height - shadow.top - shadow.bottom,
1285 };
dstockwella685a702019-01-29 02:21:541286 }
Douglas Stockwell1752f7762018-11-28 23:41:361287
1288 /**
[email protected]312112c72014-04-14 01:45:431289 * Get the coordinates of the page contents (excluding the page shadow)
1290 * relative to the screen.
rbpotter84b2076a2019-08-23 01:31:151291 * @param {number} page The index of the page to get the rect for.
1292 * @return {!ViewportRect} A rect representing the page in screen coordinates.
[email protected]312112c72014-04-14 01:45:431293 */
dstockwella685a702019-01-29 02:21:541294 getPageScreenRect(page) {
[email protected]499e9562014-06-26 05:45:271295 if (!this.documentDimensions_) {
dbeam70db0fb2017-06-19 17:09:271296 return {x: 0, y: 0, width: 0, height: 0};
[email protected]499e9562014-06-26 05:45:271297 }
Dan Beamd1cca6e2019-01-03 02:46:271298 if (page >= this.pageDimensions_.length) {
[email protected]312112c72014-04-14 01:45:431299 page = this.pageDimensions_.length - 1;
Dan Beamd1cca6e2019-01-03 02:46:271300 }
[email protected]312112c72014-04-14 01:45:431301
dstockwellafba4e42018-12-02 22:58:061302 const pageDimensions = this.pageDimensions_[page];
[email protected]312112c72014-04-14 01:45:431303
1304 // Compute the page dimensions minus the shadows.
dstockwellafba4e42018-12-02 22:58:061305 const insetDimensions = this.getPageInsetDimensions(page);
[email protected]312112c72014-04-14 01:45:431306
[email protected]345e7c62014-05-02 09:52:581307 // Compute the x-coordinate of the page within the document.
1308 // TODO(raymes): This should really be set when the PDF plugin passes the
1309 // page coordinates, but it isn't yet.
dstockwellafba4e42018-12-02 22:58:061310 const x = (this.documentDimensions_.width - pageDimensions.width) / 2 +
rbpotter81f924a42020-06-11 23:00:031311 PAGE_SHADOW.left;
[email protected]345e7c62014-05-02 09:52:581312 // Compute the space on the left of the document if the document fits
1313 // completely in the screen.
rbpotter84b2076a2019-08-23 01:31:151314 const zoom = this.getZoom();
dstockwellafba4e42018-12-02 22:58:061315 let spaceOnLeft =
rbpotter84b2076a2019-08-23 01:31:151316 (this.size.width - this.documentDimensions_.width * zoom) / 2;
[email protected]345e7c62014-05-02 09:52:581317 spaceOnLeft = Math.max(spaceOnLeft, 0);
[email protected]312112c72014-04-14 01:45:431318
1319 return {
dpapada9c2e272020-07-27 00:49:231320 x: x * zoom + spaceOnLeft - this.window_.scrollLeft,
1321 y: insetDimensions.y * zoom - this.window_.scrollTop,
rbpotter84b2076a2019-08-23 01:31:151322 width: insetDimensions.width * zoom,
1323 height: insetDimensions.height * zoom
[email protected]312112c72014-04-14 01:45:431324 };
dstockwella685a702019-01-29 02:21:541325 }
Henrique Nakashima50b18e02017-11-21 23:29:571326
1327 /**
1328 * Check if the current fitting type is a paged mode.
Henrique Nakashima50b18e02017-11-21 23:29:571329 * In a paged mode, page up and page down scroll to the top of the
1330 * previous/next page and part of the page is under the toolbar.
Henrique Nakashima50b18e02017-11-21 23:29:571331 * @return {boolean} Whether the current fitting type is a paged mode.
rbpotter81f924a42020-06-11 23:00:031332 * @private
Henrique Nakashima50b18e02017-11-21 23:29:571333 */
rbpotter81f924a42020-06-11 23:00:031334 isPagedMode_() {
Henrique Nakashima50b18e02017-11-21 23:29:571335 return (
rbpotterb43063eb2020-02-20 01:23:101336 this.fittingType_ === FittingType.FIT_TO_PAGE ||
1337 this.fittingType_ === FittingType.FIT_TO_HEIGHT);
dstockwella685a702019-01-29 02:21:541338 }
Henrique Nakashima585c7af02018-03-27 04:55:211339
rbpotterde9429d2019-09-12 00:40:081340 /**
rbpottere94435742020-06-23 00:43:321341 * Handles a navigation request to a destination from the current controller.
1342 * @param {number} page
1343 * @param {number} x
1344 * @param {number} y
1345 * @param {number} zoom
1346 */
1347 handleNavigateToDestination(page, x, y, zoom) {
1348 if (zoom) {
1349 this.setZoom(zoom);
1350 }
1351
1352 if (x || y) {
1353 this.goToPageAndXY(page, x ? x : 0, y ? y : 0);
1354 } else {
1355 this.goToPage(page);
1356 }
1357 }
1358
1359 /**
rbpotterde9429d2019-09-12 00:40:081360 * @param {!PartialPoint} point The position to which to scroll the viewport.
1361 */
dstockwella685a702019-01-29 02:21:541362 scrollTo(point) {
Henrique Nakashima585c7af02018-03-27 04:55:211363 let changed = false;
1364 const newPosition = this.position;
rbpotterb43063eb2020-02-20 01:23:101365 if (point.x !== undefined && point.x !== newPosition.x) {
Henrique Nakashima585c7af02018-03-27 04:55:211366 newPosition.x = point.x;
1367 changed = true;
1368 }
rbpotterb43063eb2020-02-20 01:23:101369 if (point.y !== undefined && point.y !== newPosition.y) {
Henrique Nakashima585c7af02018-03-27 04:55:211370 newPosition.y = point.y;
1371 changed = true;
1372 }
1373
Dan Beamd1cca6e2019-01-03 02:46:271374 if (changed) {
Henrique Nakashima585c7af02018-03-27 04:55:211375 this.position = newPosition;
Dan Beamd1cca6e2019-01-03 02:46:271376 }
dstockwella685a702019-01-29 02:21:541377 }
Henrique Nakashima585c7af02018-03-27 04:55:211378
rbpotter84b2076a2019-08-23 01:31:151379 /** @param {!Point} delta The delta by which to scroll the viewport. */
dstockwella685a702019-01-29 02:21:541380 scrollBy(delta) {
Henrique Nakashima585c7af02018-03-27 04:55:211381 const newPosition = this.position;
1382 newPosition.x += delta.x;
1383 newPosition.y += delta.y;
1384 this.scrollTo(newPosition);
[email protected]3528d6302014-02-19 08:13:071385 }
rbpotter84b2076a2019-08-23 01:31:151386
1387 /** Removes all events being tracked from the tracker. */
1388 resetTracker() {
1389 if (this.tracker_) {
1390 this.tracker_.removeAll();
1391 }
1392 }
rbpotter1e76a072020-06-09 22:05:141393
1394 /**
1395 * A callback that's called when an update to a pinch zoom is detected.
dpapadb6fbeb22020-06-12 17:06:091396 * @param {!CustomEvent<!PinchEventDetail>} e the pinch event.
rbpotter1e76a072020-06-09 22:05:141397 * @private
1398 */
1399 onPinchUpdate_(e) {
1400 // Throttle number of pinch events to one per frame.
1401 if (this.sentPinchEvent_) {
1402 return;
1403 }
1404
1405 this.sentPinchEvent_ = true;
dpapada9c2e272020-07-27 00:49:231406 window.requestAnimationFrame(() => {
rbpotter1e76a072020-06-09 22:05:141407 this.sentPinchEvent_ = false;
1408 this.mightZoom_(() => {
dpapadb6fbeb22020-06-12 17:06:091409 const {direction, center, startScaleRatio} = e.detail;
1410 this.pinchPhase_ = direction === 'out' ?
dpapadd4ce7fd2020-09-22 07:25:061411 PinchPhase.PINCH_UPDATE_ZOOM_OUT :
1412 PinchPhase.PINCH_UPDATE_ZOOM_IN;
rbpotter1e76a072020-06-09 22:05:141413
dpapadb6fbeb22020-06-12 17:06:091414 const scaleDelta = startScaleRatio / this.prevScale_;
rbpotter1e76a072020-06-09 22:05:141415 if (this.firstPinchCenterInFrame_ != null) {
1416 this.pinchPanVector_ =
dpapadb6fbeb22020-06-12 17:06:091417 vectorDelta(center, this.firstPinchCenterInFrame_);
rbpotter1e76a072020-06-09 22:05:141418 }
1419
1420 const needsScrollbars =
rbpotter81f924a42020-06-11 23:00:031421 this.documentNeedsScrollbars(this.zoomManager_.applyBrowserZoom(
rbpotter1e76a072020-06-09 22:05:141422 this.clampZoom_(this.internalZoom_ * scaleDelta)));
1423
dpapadb6fbeb22020-06-12 17:06:091424 this.pinchCenter_ = center;
rbpotter1e76a072020-06-09 22:05:141425
1426 // If there's no horizontal scrolling, keep the content centered so
1427 // the user can't zoom in on the non-content area.
1428 // TODO(mcnee) Investigate other ways of scaling when we don't have
1429 // horizontal scrolling. We want to keep the document centered,
1430 // but this causes a potentially awkward transition when we start
1431 // using the gesture center.
1432 if (!needsScrollbars.horizontal) {
1433 this.pinchCenter_ = {
dpapada9c2e272020-07-27 00:49:231434 x: this.window_.offsetWidth / 2,
1435 y: this.window_.offsetHeight / 2
rbpotter1e76a072020-06-09 22:05:141436 };
1437 } else if (this.keepContentCentered_) {
1438 this.oldCenterInContent_ =
rbpottere94435742020-06-23 00:43:321439 this.frameToContent_(this.frameToPluginCoordinate_(center));
rbpotter1e76a072020-06-09 22:05:141440 this.keepContentCentered_ = false;
1441 }
1442
Kevin McNeed88f8d82020-09-03 22:33:511443 this.fittingType_ = FittingType.NONE;
1444
rbpottere94435742020-06-23 00:43:321445 this.setPinchZoomInternal_(
1446 scaleDelta, this.frameToPluginCoordinate_(center));
rbpotter1e76a072020-06-09 22:05:141447 this.updateViewport_();
dpapadb6fbeb22020-06-12 17:06:091448 this.prevScale_ = /** @type {number} */ (startScaleRatio);
rbpotter1e76a072020-06-09 22:05:141449 });
1450 });
1451 }
1452
1453 /**
1454 * A callback that's called when the end of a pinch zoom is detected.
dpapadb6fbeb22020-06-12 17:06:091455 * @param {!CustomEvent<!PinchEventDetail>} e the pinch event.
rbpotter1e76a072020-06-09 22:05:141456 * @private
1457 */
1458 onPinchEnd_(e) {
1459 // Using rAF for pinch end prevents pinch updates scheduled by rAF getting
1460 // sent after the pinch end.
dpapada9c2e272020-07-27 00:49:231461 window.requestAnimationFrame(() => {
rbpotter1e76a072020-06-09 22:05:141462 this.mightZoom_(() => {
dpapadb6fbeb22020-06-12 17:06:091463 const {center, startScaleRatio} = e.detail;
dpapadd4ce7fd2020-09-22 07:25:061464 this.pinchPhase_ = PinchPhase.PINCH_END;
dpapadb6fbeb22020-06-12 17:06:091465 const scaleDelta = startScaleRatio / this.prevScale_;
1466 this.pinchCenter_ = /** @type {!Point} */ (center);
rbpotter1e76a072020-06-09 22:05:141467
rbpottere94435742020-06-23 00:43:321468 this.setPinchZoomInternal_(
1469 scaleDelta, this.frameToPluginCoordinate_(center));
rbpotter1e76a072020-06-09 22:05:141470 this.updateViewport_();
1471 });
1472
dpapadd4ce7fd2020-09-22 07:25:061473 this.pinchPhase_ = PinchPhase.PINCH_NONE;
rbpotter1e76a072020-06-09 22:05:141474 this.pinchPanVector_ = null;
1475 this.pinchCenter_ = null;
1476 this.firstPinchCenterInFrame_ = null;
1477 });
1478 }
1479
1480 /**
1481 * A callback that's called when the start of a pinch zoom is detected.
dpapadb6fbeb22020-06-12 17:06:091482 * @param {!CustomEvent<!PinchEventDetail>} e the pinch event.
rbpotter1e76a072020-06-09 22:05:141483 * @private
1484 */
1485 onPinchStart_(e) {
1486 // We also use rAF for pinch start, so that if there is a pinch end event
1487 // scheduled by rAF, this pinch start will be sent after.
dpapada9c2e272020-07-27 00:49:231488 window.requestAnimationFrame(() => {
dpapadd4ce7fd2020-09-22 07:25:061489 this.pinchPhase_ = PinchPhase.PINCH_START;
rbpotter1e76a072020-06-09 22:05:141490 this.prevScale_ = 1;
1491 this.oldCenterInContent_ =
rbpottere94435742020-06-23 00:43:321492 this.frameToContent_(this.frameToPluginCoordinate_(e.detail.center));
rbpotter1e76a072020-06-09 22:05:141493
rbpotter81f924a42020-06-11 23:00:031494 const needsScrollbars = this.documentNeedsScrollbars(this.getZoom());
rbpotter1e76a072020-06-09 22:05:141495 this.keepContentCentered_ = !needsScrollbars.horizontal;
1496 // We keep track of beginning of the pinch.
1497 // By doing so we will be able to compute the pan distance.
dpapadb6fbeb22020-06-12 17:06:091498 this.firstPinchCenterInFrame_ = e.detail.center;
rbpotter1e76a072020-06-09 22:05:141499 });
1500 }
Kevin McNeed88f8d82020-09-03 22:33:511501
1502 /** @return {!GestureDetector} */
1503 getGestureDetectorForTesting() {
1504 return this.gestureDetector_;
1505 }
dstockwella685a702019-01-29 02:21:541506}
rbpotter84b2076a2019-08-23 01:31:151507
1508/**
1509 * Enumeration of pinch states.
1510 * This should match PinchPhase enum in pdf/out_of_process_instance.h
1511 * @enum {number}
1512 */
dpapadd4ce7fd2020-09-22 07:25:061513export const PinchPhase = {
rbpotter84b2076a2019-08-23 01:31:151514 PINCH_NONE: 0,
1515 PINCH_START: 1,
1516 PINCH_UPDATE_ZOOM_OUT: 2,
1517 PINCH_UPDATE_ZOOM_IN: 3,
1518 PINCH_END: 4
1519};
1520
rbpotter81f924a42020-06-11 23:00:031521/**
1522 * The increment to scroll a page by in pixels when up/down/left/right arrow
1523 * keys are pressed. Usually we just let the browser handle scrolling on the
1524 * window when these keys are pressed but in certain cases we need to simulate
1525 * these events.
1526 * @type {number}
1527 */
1528const SCROLL_INCREMENT = 40;
rbpotter84b2076a2019-08-23 01:31:151529
rbpotter81f924a42020-06-11 23:00:031530/**
1531 * The width of the page shadow around pages in pixels.
1532 * @type {!{top: number, bottom: number, left: number, right: number}}
1533 */
1534export const PAGE_SHADOW = {
rbpotter84b2076a2019-08-23 01:31:151535 top: 3,
1536 bottom: 7,
1537 left: 5,
1538 right: 5
1539};