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.topToolbarHeight_
>
142 this.window_
.innerHeight
147 * Returns true if the document needs scrollbars at the current zoom level.
148 * @return {Object} with 'x' and 'y' keys which map to bool values
149 * indicating if the horizontal and vertical scrollbars are needed
152 documentHasScrollbars: function() {
153 return this.documentNeedsScrollbars_(this.zoom_
);
158 * Helper function called when the zoomed document size changes.
160 contentSizeChanged_: function() {
161 var zoomedDimensions
= this.getZoomedDocumentDimensions_(this.zoom_
);
162 if (zoomedDimensions
) {
163 this.sizer_
.style
.width
= zoomedDimensions
.width
+ 'px';
164 this.sizer_
.style
.height
= zoomedDimensions
.height
+
165 this.topToolbarHeight_
+ 'px';
171 * Called when the viewport should be updated.
173 updateViewport_: function() {
174 this.viewportChangedCallback_();
179 * Called when the viewport size changes.
181 resize_: function() {
182 if (this.fittingType_
== Viewport
.FittingType
.FIT_TO_PAGE
)
183 this.fitToPageInternal_(false);
184 else if (this.fittingType_
== Viewport
.FittingType
.FIT_TO_WIDTH
)
187 this.updateViewport_();
191 * @type {Object} the scroll position of the viewport.
195 x
: this.window_
.pageXOffset
,
196 y
: this.window_
.pageYOffset
- this.topToolbarHeight_
201 * Scroll the viewport to the specified position.
202 * @type {Object} position the position to scroll to.
204 set position(position
) {
205 this.window_
.scrollTo(position
.x
, position
.y
+ this.topToolbarHeight_
);
209 * @type {Object} the size of the viewport excluding scrollbars.
212 var needsScrollbars
= this.documentNeedsScrollbars_(this.zoom_
);
213 var scrollbarWidth
= needsScrollbars
.vertical
? this.scrollbarWidth_
: 0;
214 var scrollbarHeight
= needsScrollbars
.horizontal
? this.scrollbarWidth_
: 0;
216 width
: this.window_
.innerWidth
- scrollbarWidth
,
217 height
: this.window_
.innerHeight
- scrollbarHeight
222 * @type {number} the zoom level of the viewport.
230 * Used to wrap a function that might perform zooming on the viewport. This is
231 * required so that we can notify the plugin that zooming is in progress
232 * so that while zooming is taking place it can stop reacting to scroll events
233 * from the viewport. This is to avoid flickering.
235 mightZoom_: function(f
) {
236 this.beforeZoomCallback_();
237 this.allowedToChangeZoom_
= true;
239 this.allowedToChangeZoom_
= false;
240 this.afterZoomCallback_();
245 * Sets the zoom of the viewport.
246 * @param {number} newZoom the zoom level to zoom to.
248 setZoomInternal_: function(newZoom
) {
249 if (!this.allowedToChangeZoom_
) {
250 throw 'Called Viewport.setZoomInternal_ without calling ' +
251 'Viewport.mightZoom_.';
253 // Record the scroll position (relative to the top-left of the window).
254 var currentScrollPos
= {
255 x
: this.position
.x
/ this.zoom_
,
256 y
: this.position
.y
/ this.zoom_
258 this.zoom_
= newZoom
;
259 this.contentSizeChanged_();
260 // Scroll to the scaled scroll position.
262 x
: currentScrollPos
.x
* newZoom
,
263 y
: currentScrollPos
.y
* newZoom
268 * Sets the zoom to the given zoom level.
269 * @param {number} newZoom the zoom level to zoom to.
271 setZoom: function(newZoom
) {
272 this.fittingType_
= Viewport
.FittingType
.NONE
;
273 newZoom
= Math
.max(Viewport
.ZOOM_FACTOR_RANGE
.min
,
274 Math
.min(newZoom
, Viewport
.ZOOM_FACTOR_RANGE
.max
));
275 this.mightZoom_(function() {
276 this.setZoomInternal_(newZoom
);
277 this.updateViewport_();
282 * @type {number} the width of scrollbars in the viewport in pixels.
284 get scrollbarWidth() {
285 return this.scrollbarWidth_
;
289 * @type {Viewport.FittingType} the fitting type the viewport is currently in.
292 return this.fittingType_
;
297 * @param {integer} y the y-coordinate to get the page at.
298 * @return {integer} the index of a page overlapping the given y-coordinate.
300 getPageAtY_: function(y
) {
302 var max
= this.pageDimensions_
.length
- 1;
304 var page
= Math
.floor(min
+ ((max
- min
) / 2));
305 // There might be a gap between the pages, in which case use the bottom
306 // of the previous page as the top for finding the page.
309 top
= this.pageDimensions_
[page
- 1].y
+
310 this.pageDimensions_
[page
- 1].height
;
312 var bottom
= this.pageDimensions_
[page
].y
+
313 this.pageDimensions_
[page
].height
;
315 if (top
<= y
&& bottom
> y
)
326 * Returns the page with the greatest proportion of its height in the current
328 * @return {int} the index of the most visible page.
330 getMostVisiblePage: function() {
331 var firstVisiblePage
= this.getPageAtY_(this.position
.y
/ this.zoom_
);
332 if (firstVisiblePage
== this.pageDimensions_
.length
- 1)
333 return firstVisiblePage
;
336 x
: this.position
.x
/ this.zoom_
,
337 y
: this.position
.y
/ this.zoom_
,
338 width
: this.size
.width
/ this.zoom_
,
339 height
: this.size
.height
/ this.zoom_
341 var firstVisiblePageVisibility
= getIntersectionHeight(
342 this.pageDimensions_
[firstVisiblePage
], viewportRect
) /
343 this.pageDimensions_
[firstVisiblePage
].height
;
344 var nextPageVisibility
= getIntersectionHeight(
345 this.pageDimensions_
[firstVisiblePage
+ 1], viewportRect
) /
346 this.pageDimensions_
[firstVisiblePage
+ 1].height
;
347 if (nextPageVisibility
> firstVisiblePageVisibility
)
348 return firstVisiblePage
+ 1;
349 return firstVisiblePage
;
354 * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is
355 * the dimensions for a given page and if |widthOnly| is true, it indicates
356 * that fit-to-page zoom should be computed rather than fit-to-page.
357 * @param {Object} pageDimensions the dimensions of a given page
358 * @param {boolean} widthOnly a bool indicating whether fit-to-page or
359 * fit-to-width should be computed.
360 * @return {number} the zoom to use
362 computeFittingZoom_: function(pageDimensions
, widthOnly
) {
363 // First compute the zoom without scrollbars.
364 var zoomWidth
= this.window_
.innerWidth
/ pageDimensions
.width
;
370 zoomHeight
= this.window_
.innerHeight
/ pageDimensions
.height
;
371 zoom
= Math
.min(zoomWidth
, zoomHeight
);
373 // Check if there needs to be any scrollbars.
374 var needsScrollbars
= this.documentNeedsScrollbars_(zoom
);
376 // If the document fits, just return the zoom.
377 if (!needsScrollbars
.horizontal
&& !needsScrollbars
.vertical
)
380 var zoomedDimensions
= this.getZoomedDocumentDimensions_(zoom
);
382 // Check if adding a scrollbar will result in needing the other scrollbar.
383 var scrollbarWidth
= this.scrollbarWidth_
;
384 if (needsScrollbars
.horizontal
&&
385 zoomedDimensions
.height
> this.window_
.innerHeight
- scrollbarWidth
) {
386 needsScrollbars
.vertical
= true;
388 if (needsScrollbars
.vertical
&&
389 zoomedDimensions
.width
> this.window_
.innerWidth
- scrollbarWidth
) {
390 needsScrollbars
.horizontal
= true;
393 // Compute available window space.
394 var windowWithScrollbars
= {
395 width
: this.window_
.innerWidth
,
396 height
: this.window_
.innerHeight
398 if (needsScrollbars
.horizontal
)
399 windowWithScrollbars
.height
-= scrollbarWidth
;
400 if (needsScrollbars
.vertical
)
401 windowWithScrollbars
.width
-= scrollbarWidth
;
403 // Recompute the zoom.
404 zoomWidth
= windowWithScrollbars
.width
/ pageDimensions
.width
;
408 zoomHeight
= windowWithScrollbars
.height
/ pageDimensions
.height
;
409 zoom
= Math
.min(zoomWidth
, zoomHeight
);
415 * Zoom the viewport so that the page-width consumes the entire viewport.
417 fitToWidth: function() {
418 this.mightZoom_(function() {
419 this.fittingType_
= Viewport
.FittingType
.FIT_TO_WIDTH
;
420 if (!this.documentDimensions_
)
422 // When computing fit-to-width, the maximum width of a page in the
423 // document is used, which is equal to the size of the document width.
424 this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_
,
426 var page
= this.getMostVisiblePage();
427 this.updateViewport_();
433 * Zoom the viewport so that a page consumes the entire viewport.
434 * @param {boolean} scrollToTopOfPage Set to true if the viewport should be
435 * scrolled to the top of the current page. Set to false if the viewport
436 * should remain at the current scroll position.
438 fitToPageInternal_: function(scrollToTopOfPage
) {
439 this.mightZoom_(function() {
440 this.fittingType_
= Viewport
.FittingType
.FIT_TO_PAGE
;
441 if (!this.documentDimensions_
)
443 var page
= this.getMostVisiblePage();
444 // Fit to the current page's height and the widest page's width.
446 width
: this.documentDimensions_
.width
,
447 height
: this.pageDimensions_
[page
].height
,
449 this.setZoomInternal_(this.computeFittingZoom_(dimensions
, false));
450 if (scrollToTopOfPage
) {
453 y
: this.pageDimensions_
[page
].y
* this.zoom_
456 this.updateViewport_();
461 * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
462 * the viewport to the top of the current page.
464 fitToPage: function() {
465 this.fitToPageInternal_(true);
469 * Zoom out to the next predefined zoom level.
471 zoomOut: function() {
472 this.mightZoom_(function() {
473 this.fittingType_
= Viewport
.FittingType
.NONE
;
474 var nextZoom
= Viewport
.ZOOM_FACTORS
[0];
475 for (var i
= 0; i
< Viewport
.ZOOM_FACTORS
.length
; i
++) {
476 if (Viewport
.ZOOM_FACTORS
[i
] < this.zoom_
)
477 nextZoom
= Viewport
.ZOOM_FACTORS
[i
];
479 this.setZoomInternal_(nextZoom
);
480 this.updateViewport_();
485 * Zoom in to the next predefined zoom level.
488 this.mightZoom_(function() {
489 this.fittingType_
= Viewport
.FittingType
.NONE
;
490 var nextZoom
= Viewport
.ZOOM_FACTORS
[Viewport
.ZOOM_FACTORS
.length
- 1];
491 for (var i
= Viewport
.ZOOM_FACTORS
.length
- 1; i
>= 0; i
--) {
492 if (Viewport
.ZOOM_FACTORS
[i
] > this.zoom_
)
493 nextZoom
= Viewport
.ZOOM_FACTORS
[i
];
495 this.setZoomInternal_(nextZoom
);
496 this.updateViewport_();
501 * Go to the given page index.
502 * @param {number} page the index of the page to go to. zero-based.
504 goToPage: function(page
) {
505 this.mightZoom_(function() {
506 if (this.pageDimensions_
.length
=== 0)
510 if (page
>= this.pageDimensions_
.length
)
511 page
= this.pageDimensions_
.length
- 1;
512 var dimensions
= this.pageDimensions_
[page
];
513 var toolbarOffset
= 0;
514 // Unless we're in fit to page mode, scroll above the page by
515 // |this.topToolbarHeight_| so that the toolbar isn't covering it
517 if (this.fittingType_
!= Viewport
.FittingType
.FIT_TO_PAGE
)
518 toolbarOffset
= this.topToolbarHeight_
;
520 x
: dimensions
.x
* this.zoom_
,
521 y
: dimensions
.y
* this.zoom_
- toolbarOffset
523 this.updateViewport_();
528 * Set the dimensions of the document.
529 * @param {Object} documentDimensions the dimensions of the document
531 setDocumentDimensions: function(documentDimensions
) {
532 this.mightZoom_(function() {
533 var initialDimensions
= !this.documentDimensions_
;
534 this.documentDimensions_
= documentDimensions
;
535 this.pageDimensions_
= this.documentDimensions_
.pageDimensions
;
536 if (initialDimensions
) {
537 this.setZoomInternal_(
538 Math
.min(this.defaultZoom_
,
539 this.computeFittingZoom_(this.documentDimensions_
, true)));
542 y
: -this.topToolbarHeight_
545 this.contentSizeChanged_();
551 * Get the coordinates of the page contents (excluding the page shadow)
552 * relative to the screen.
553 * @param {number} page the index of the page to get the rect for.
554 * @return {Object} a rect representing the page in screen coordinates.
556 getPageScreenRect: function(page
) {
557 if (!this.documentDimensions_
) {
565 if (page
>= this.pageDimensions_
.length
)
566 page
= this.pageDimensions_
.length
- 1;
568 var pageDimensions
= this.pageDimensions_
[page
];
570 // Compute the page dimensions minus the shadows.
571 var insetDimensions
= {
572 x
: pageDimensions
.x
+ Viewport
.PAGE_SHADOW
.left
,
573 y
: pageDimensions
.y
+ Viewport
.PAGE_SHADOW
.top
,
574 width
: pageDimensions
.width
- Viewport
.PAGE_SHADOW
.left
-
575 Viewport
.PAGE_SHADOW
.right
,
576 height
: pageDimensions
.height
- Viewport
.PAGE_SHADOW
.top
-
577 Viewport
.PAGE_SHADOW
.bottom
580 // Compute the x-coordinate of the page within the document.
581 // TODO(raymes): This should really be set when the PDF plugin passes the
582 // page coordinates, but it isn't yet.
583 var x
= (this.documentDimensions_
.width
- pageDimensions
.width
) / 2 +
584 Viewport
.PAGE_SHADOW
.left
;
585 // Compute the space on the left of the document if the document fits
586 // completely in the screen.
587 var spaceOnLeft
= (this.size
.width
-
588 this.documentDimensions_
.width
* this.zoom_
) / 2;
589 spaceOnLeft
= Math
.max(spaceOnLeft
, 0);
592 x
: x
* this.zoom_
+ spaceOnLeft
- this.window_
.pageXOffset
,
593 y
: insetDimensions
.y
* this.zoom_
- this.window_
.pageYOffset
,
594 width
: insetDimensions
.width
* this.zoom_
,
595 height
: insetDimensions
.height
* this.zoom_