1 // 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.
6 * Returns the height of the intersection of two rectangles.
7 * @param {Object} rect1 the first rect
8 * @param {Object} rect2 the second rect
9 * @return {number} the height of the intersection of the rects
11 function getIntersectionHeight(rect1
, rect2
) {
13 Math
.min(rect1
.y
+ rect1
.height
, rect2
.y
+ rect2
.height
) -
14 Math
.max(rect1
.y
, rect2
.y
));
18 * Create a new viewport.
20 * @param {Window} window the window
21 * @param {Object} sizer is the element which represents the size of the
22 * document in the viewport
23 * @param {Function} viewportChangedCallback is run when the viewport changes
24 * @param {Function} beforeZoomCallback is run before a change in zoom
25 * @param {Function} afterZoomCallback is run after a change in zoom
26 * @param {number} scrollbarWidth the width of scrollbars on the page
27 * @param {number} defaultZoom The default zoom level.
28 * @param {number} topToolbarHeight The number of pixels that should initially
29 * be left blank above the document for the toolbar.
31 function Viewport(window
,
33 viewportChangedCallback
,
39 this.window_
= window
;
41 this.viewportChangedCallback_
= viewportChangedCallback
;
42 this.beforeZoomCallback_
= beforeZoomCallback
;
43 this.afterZoomCallback_
= afterZoomCallback
;
44 this.allowedToChangeZoom_
= false;
46 this.documentDimensions_
= null;
47 this.pageDimensions_
= [];
48 this.scrollbarWidth_
= scrollbarWidth
;
49 this.fittingType_
= Viewport
.FittingType
.NONE
;
50 this.defaultZoom_
= defaultZoom
;
51 this.topToolbarHeight_
= topToolbarHeight
;
53 window
.addEventListener('scroll', this.updateViewport_
.bind(this));
54 window
.addEventListener('resize', this.resize_
.bind(this));
58 * Enumeration of page fitting types.
61 Viewport
.FittingType
= {
63 FIT_TO_PAGE
: 'fit-to-page',
64 FIT_TO_WIDTH
: 'fit-to-width'
68 * The increment to scroll a page by in pixels when up/down/left/right arrow
69 * keys are pressed. Usually we just let the browser handle scrolling on the
70 * window when these keys are pressed but in certain cases we need to simulate
73 Viewport
.SCROLL_INCREMENT
= 40;
76 * Predefined zoom factors to be used when zooming in/out. These are in
77 * ascending order. This should match the list in
78 * components/ui/zoom/page_zoom_constants.h
80 Viewport
.ZOOM_FACTORS
= [0.25, 0.333, 0.5, 0.666, 0.75, 0.9, 1,
81 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
84 * The minimum and maximum range to be used to clip zoom factor.
86 Viewport
.ZOOM_FACTOR_RANGE
= {
87 min
: Viewport
.ZOOM_FACTORS
[0],
88 max
: Viewport
.ZOOM_FACTORS
[Viewport
.ZOOM_FACTORS
.length
- 1]
92 * The width of the page shadow around pages in pixels.
94 Viewport
.PAGE_SHADOW
= {top
: 3, bottom
: 7, left
: 5, right
: 5};
96 Viewport
.prototype = {
98 * Returns the zoomed and rounded document dimensions for the given zoom.
99 * Rounding is necessary when interacting with the renderer which tends to
100 * operate in integral values (for example for determining if scrollbars
102 * @param {number} zoom The zoom to use to compute the scaled dimensions.
103 * @return {Object} A dictionary with scaled 'width'/'height' of the document.
106 getZoomedDocumentDimensions_: function(zoom
) {
107 if (!this.documentDimensions_
)
110 width
: Math
.round(this.documentDimensions_
.width
* zoom
),
111 height
: Math
.round(this.documentDimensions_
.height
* zoom
)
117 * Returns true if the document needs scrollbars at the given zoom level.
118 * @param {number} zoom compute whether scrollbars are needed at this zoom
119 * @return {Object} with 'horizontal' and 'vertical' keys which map to bool
120 * values indicating if the horizontal and vertical scrollbars are needed
123 documentNeedsScrollbars_: function(zoom
) {
124 var zoomedDimensions
= this.getZoomedDocumentDimensions_(zoom
);
125 if (!zoomedDimensions
) {
132 // If scrollbars are required for one direction, expand the document in the
133 // other direction to take the width of the scrollbars into account when
134 // deciding whether the other direction needs scrollbars.
135 if (zoomedDimensions
.width
> this.window_
.innerWidth
)
136 zoomedDimensions
.height
+= this.scrollbarWidth_
;
137 else if (zoomedDimensions
.height
> this.window_
.innerHeight
)
138 zoomedDimensions
.width
+= this.scrollbarWidth_
;
140 horizontal
: zoomedDimensions
.width
> this.window_
.innerWidth
,
141 vertical
: zoomedDimensions
.height
> this.window_
.innerHeight
146 * Returns true if the document needs scrollbars at the current zoom level.
147 * @return {Object} with 'x' and 'y' keys which map to bool values
148 * indicating if the horizontal and vertical scrollbars are needed
151 documentHasScrollbars: function() {
152 return this.documentNeedsScrollbars_(this.zoom_
);
157 * Helper function called when the zoomed document size changes.
159 contentSizeChanged_: function() {
160 var zoomedDimensions
= this.getZoomedDocumentDimensions_(this.zoom_
);
161 if (zoomedDimensions
) {
162 this.sizer_
.style
.width
= zoomedDimensions
.width
+ 'px';
163 this.sizer_
.style
.height
= zoomedDimensions
.height
+
164 this.topToolbarHeight_
+ 'px';
170 * Called when the viewport should be updated.
172 updateViewport_: function() {
173 this.viewportChangedCallback_();
178 * Called when the viewport size changes.
180 resize_: function() {
181 if (this.fittingType_
== Viewport
.FittingType
.FIT_TO_PAGE
)
182 this.fitToPageInternal_(false);
183 else if (this.fittingType_
== Viewport
.FittingType
.FIT_TO_WIDTH
)
186 this.updateViewport_();
190 * @type {Object} the scroll position of the viewport.
194 x
: this.window_
.pageXOffset
,
195 y
: this.window_
.pageYOffset
- this.topToolbarHeight_
200 * Scroll the viewport to the specified position.
201 * @type {Object} position the position to scroll to.
203 set position(position
) {
204 this.window_
.scrollTo(position
.x
, position
.y
+ this.topToolbarHeight_
);
208 * @type {Object} the size of the viewport excluding scrollbars.
211 var needsScrollbars
= this.documentNeedsScrollbars_(this.zoom_
);
212 var scrollbarWidth
= needsScrollbars
.vertical
? this.scrollbarWidth_
: 0;
213 var scrollbarHeight
= needsScrollbars
.horizontal
? this.scrollbarWidth_
: 0;
215 width
: this.window_
.innerWidth
- scrollbarWidth
,
216 height
: this.window_
.innerHeight
- scrollbarHeight
221 * @type {number} the zoom level of the viewport.
229 * Used to wrap a function that might perform zooming on the viewport. This is
230 * required so that we can notify the plugin that zooming is in progress
231 * so that while zooming is taking place it can stop reacting to scroll events
232 * from the viewport. This is to avoid flickering.
234 mightZoom_: function(f
) {
235 this.beforeZoomCallback_();
236 this.allowedToChangeZoom_
= true;
238 this.allowedToChangeZoom_
= false;
239 this.afterZoomCallback_();
244 * Sets the zoom of the viewport.
245 * @param {number} newZoom the zoom level to zoom to.
247 setZoomInternal_: function(newZoom
) {
248 if (!this.allowedToChangeZoom_
) {
249 throw 'Called Viewport.setZoomInternal_ without calling ' +
250 'Viewport.mightZoom_.';
252 // Record the scroll position (relative to the top-left of the window).
253 var currentScrollPos
= {
254 x
: this.position
.x
/ this.zoom_
,
255 y
: this.position
.y
/ this.zoom_
257 this.zoom_
= newZoom
;
258 this.contentSizeChanged_();
259 // Scroll to the scaled scroll position.
261 x
: currentScrollPos
.x
* newZoom
,
262 y
: currentScrollPos
.y
* newZoom
267 * Sets the zoom to the given zoom level.
268 * @param {number} newZoom the zoom level to zoom to.
270 setZoom: function(newZoom
) {
271 this.fittingType_
= Viewport
.FittingType
.NONE
;
272 newZoom
= Math
.max(Viewport
.ZOOM_FACTOR_RANGE
.min
,
273 Math
.min(newZoom
, Viewport
.ZOOM_FACTOR_RANGE
.max
));
274 this.mightZoom_(function() {
275 this.setZoomInternal_(newZoom
);
276 this.updateViewport_();
281 * @type {number} the width of scrollbars in the viewport in pixels.
283 get scrollbarWidth() {
284 return this.scrollbarWidth_
;
288 * @type {Viewport.FittingType} the fitting type the viewport is currently in.
291 return this.fittingType_
;
296 * @param {integer} y the y-coordinate to get the page at.
297 * @return {integer} the index of a page overlapping the given y-coordinate.
299 getPageAtY_: function(y
) {
301 var max
= this.pageDimensions_
.length
- 1;
303 var page
= Math
.floor(min
+ ((max
- min
) / 2));
304 // There might be a gap between the pages, in which case use the bottom
305 // of the previous page as the top for finding the page.
308 top
= this.pageDimensions_
[page
- 1].y
+
309 this.pageDimensions_
[page
- 1].height
;
311 var bottom
= this.pageDimensions_
[page
].y
+
312 this.pageDimensions_
[page
].height
;
314 if (top
<= y
&& bottom
> y
)
325 * Returns the page with the greatest proportion of its height in the current
327 * @return {int} the index of the most visible page.
329 getMostVisiblePage: function() {
330 var firstVisiblePage
= this.getPageAtY_(this.position
.y
/ this.zoom_
);
331 if (firstVisiblePage
== this.pageDimensions_
.length
- 1)
332 return firstVisiblePage
;
335 x
: this.position
.x
/ this.zoom_
,
336 y
: this.position
.y
/ this.zoom_
,
337 width
: this.size
.width
/ this.zoom_
,
338 height
: this.size
.height
/ this.zoom_
340 var firstVisiblePageVisibility
= getIntersectionHeight(
341 this.pageDimensions_
[firstVisiblePage
], viewportRect
) /
342 this.pageDimensions_
[firstVisiblePage
].height
;
343 var nextPageVisibility
= getIntersectionHeight(
344 this.pageDimensions_
[firstVisiblePage
+ 1], viewportRect
) /
345 this.pageDimensions_
[firstVisiblePage
+ 1].height
;
346 if (nextPageVisibility
> firstVisiblePageVisibility
)
347 return firstVisiblePage
+ 1;
348 return firstVisiblePage
;
353 * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is
354 * the dimensions for a given page and if |widthOnly| is true, it indicates
355 * that fit-to-page zoom should be computed rather than fit-to-page.
356 * @param {Object} pageDimensions the dimensions of a given page
357 * @param {boolean} widthOnly a bool indicating whether fit-to-page or
358 * fit-to-width should be computed.
359 * @return {number} the zoom to use
361 computeFittingZoom_: function(pageDimensions
, widthOnly
) {
362 // First compute the zoom without scrollbars.
363 var zoomWidth
= this.window_
.innerWidth
/ pageDimensions
.width
;
369 zoomHeight
= this.window_
.innerHeight
/ pageDimensions
.height
;
370 zoom
= Math
.min(zoomWidth
, zoomHeight
);
372 // Check if there needs to be any scrollbars.
373 var needsScrollbars
= this.documentNeedsScrollbars_(zoom
);
375 // If the document fits, just return the zoom.
376 if (!needsScrollbars
.horizontal
&& !needsScrollbars
.vertical
)
379 var zoomedDimensions
= this.getZoomedDocumentDimensions_(zoom
);
381 // Check if adding a scrollbar will result in needing the other scrollbar.
382 var scrollbarWidth
= this.scrollbarWidth_
;
383 if (needsScrollbars
.horizontal
&&
384 zoomedDimensions
.height
> this.window_
.innerHeight
- scrollbarWidth
) {
385 needsScrollbars
.vertical
= true;
387 if (needsScrollbars
.vertical
&&
388 zoomedDimensions
.width
> this.window_
.innerWidth
- scrollbarWidth
) {
389 needsScrollbars
.horizontal
= true;
392 // Compute available window space.
393 var windowWithScrollbars
= {
394 width
: this.window_
.innerWidth
,
395 height
: this.window_
.innerHeight
397 if (needsScrollbars
.horizontal
)
398 windowWithScrollbars
.height
-= scrollbarWidth
;
399 if (needsScrollbars
.vertical
)
400 windowWithScrollbars
.width
-= scrollbarWidth
;
402 // Recompute the zoom.
403 zoomWidth
= windowWithScrollbars
.width
/ pageDimensions
.width
;
407 zoomHeight
= windowWithScrollbars
.height
/ pageDimensions
.height
;
408 zoom
= Math
.min(zoomWidth
, zoomHeight
);
414 * Zoom the viewport so that the page-width consumes the entire viewport.
416 fitToWidth: function() {
417 this.mightZoom_(function() {
418 this.fittingType_
= Viewport
.FittingType
.FIT_TO_WIDTH
;
419 if (!this.documentDimensions_
)
421 // When computing fit-to-width, the maximum width of a page in the
422 // document is used, which is equal to the size of the document width.
423 this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_
,
425 var page
= this.getMostVisiblePage();
426 this.updateViewport_();
432 * Zoom the viewport so that a page consumes the entire viewport.
433 * @param {boolean} scrollToTopOfPage Set to true if the viewport should be
434 * scrolled to the top of the current page. Set to false if the viewport
435 * should remain at the current scroll position.
437 fitToPageInternal_: function(scrollToTopOfPage
) {
438 this.mightZoom_(function() {
439 this.fittingType_
= Viewport
.FittingType
.FIT_TO_PAGE
;
440 if (!this.documentDimensions_
)
442 var page
= this.getMostVisiblePage();
443 // Fit to the current page's height and the widest page's width.
445 width
: this.documentDimensions_
.width
,
446 height
: this.pageDimensions_
[page
].height
,
448 this.setZoomInternal_(this.computeFittingZoom_(dimensions
, false));
449 if (scrollToTopOfPage
) {
452 y
: this.pageDimensions_
[page
].y
* this.zoom_
455 this.updateViewport_();
460 * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
461 * the viewport to the top of the current page.
463 fitToPage: function() {
464 this.fitToPageInternal_(true);
468 * Zoom out to the next predefined zoom level.
470 zoomOut: function() {
471 this.mightZoom_(function() {
472 this.fittingType_
= Viewport
.FittingType
.NONE
;
473 var nextZoom
= Viewport
.ZOOM_FACTORS
[0];
474 for (var i
= 0; i
< Viewport
.ZOOM_FACTORS
.length
; i
++) {
475 if (Viewport
.ZOOM_FACTORS
[i
] < this.zoom_
)
476 nextZoom
= Viewport
.ZOOM_FACTORS
[i
];
478 this.setZoomInternal_(nextZoom
);
479 this.updateViewport_();
484 * Zoom in to the next predefined zoom level.
487 this.mightZoom_(function() {
488 this.fittingType_
= Viewport
.FittingType
.NONE
;
489 var nextZoom
= Viewport
.ZOOM_FACTORS
[Viewport
.ZOOM_FACTORS
.length
- 1];
490 for (var i
= Viewport
.ZOOM_FACTORS
.length
- 1; i
>= 0; i
--) {
491 if (Viewport
.ZOOM_FACTORS
[i
] > this.zoom_
)
492 nextZoom
= Viewport
.ZOOM_FACTORS
[i
];
494 this.setZoomInternal_(nextZoom
);
495 this.updateViewport_();
500 * Go to the given page index.
501 * @param {number} page the index of the page to go to. zero-based.
503 goToPage: function(page
) {
504 this.mightZoom_(function() {
505 if (this.pageDimensions_
.length
=== 0)
509 if (page
>= this.pageDimensions_
.length
)
510 page
= this.pageDimensions_
.length
- 1;
511 var dimensions
= this.pageDimensions_
[page
];
512 var toolbarOffset
= 0;
513 // Unless we're in fit to page mode, scroll above the page by
514 // |this.topToolbarHeight_| so that the toolbar isn't covering it
516 if (this.fittingType_
!= Viewport
.FittingType
.FIT_TO_PAGE
)
517 toolbarOffset
= this.topToolbarHeight_
;
519 x
: dimensions
.x
* this.zoom_
,
520 y
: dimensions
.y
* this.zoom_
- toolbarOffset
522 this.updateViewport_();
527 * Set the dimensions of the document.
528 * @param {Object} documentDimensions the dimensions of the document
530 setDocumentDimensions: function(documentDimensions
) {
531 this.mightZoom_(function() {
532 var initialDimensions
= !this.documentDimensions_
;
533 this.documentDimensions_
= documentDimensions
;
534 this.pageDimensions_
= this.documentDimensions_
.pageDimensions
;
535 if (initialDimensions
) {
536 this.setZoomInternal_(
537 Math
.min(this.defaultZoom_
,
538 this.computeFittingZoom_(this.documentDimensions_
, true)));
541 y
: -this.topToolbarHeight_
544 this.contentSizeChanged_();
550 * Get the coordinates of the page contents (excluding the page shadow)
551 * relative to the screen.
552 * @param {number} page the index of the page to get the rect for.
553 * @return {Object} a rect representing the page in screen coordinates.
555 getPageScreenRect: function(page
) {
556 if (!this.documentDimensions_
) {
564 if (page
>= this.pageDimensions_
.length
)
565 page
= this.pageDimensions_
.length
- 1;
567 var pageDimensions
= this.pageDimensions_
[page
];
569 // Compute the page dimensions minus the shadows.
570 var insetDimensions
= {
571 x
: pageDimensions
.x
+ Viewport
.PAGE_SHADOW
.left
,
572 y
: pageDimensions
.y
+ Viewport
.PAGE_SHADOW
.top
,
573 width
: pageDimensions
.width
- Viewport
.PAGE_SHADOW
.left
-
574 Viewport
.PAGE_SHADOW
.right
,
575 height
: pageDimensions
.height
- Viewport
.PAGE_SHADOW
.top
-
576 Viewport
.PAGE_SHADOW
.bottom
579 // Compute the x-coordinate of the page within the document.
580 // TODO(raymes): This should really be set when the PDF plugin passes the
581 // page coordinates, but it isn't yet.
582 var x
= (this.documentDimensions_
.width
- pageDimensions
.width
) / 2 +
583 Viewport
.PAGE_SHADOW
.left
;
584 // Compute the space on the left of the document if the document fits
585 // completely in the screen.
586 var spaceOnLeft
= (this.size
.width
-
587 this.documentDimensions_
.width
* this.zoom_
) / 2;
588 spaceOnLeft
= Math
.max(spaceOnLeft
, 0);
591 x
: x
* this.zoom_
+ spaceOnLeft
- this.window_
.pageXOffset
,
592 y
: insetDimensions
.y
* this.zoom_
- this.window_
.pageYOffset
,
593 width
: insetDimensions
.width
* this.zoom_
,
594 height
: insetDimensions
.height
* this.zoom_