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 = {
99 * Returns true if the document needs scrollbars at the given zoom level.
100 * @param {number} zoom compute whether scrollbars are needed at this zoom
101 * @return {Object} with 'horizontal' and 'vertical' keys which map to bool
102 * values indicating if the horizontal and vertical scrollbars are needed
105 documentNeedsScrollbars_: function(zoom
) {
106 if (!this.documentDimensions_
) {
112 var documentWidth
= this.documentDimensions_
.width
* zoom
;
113 var documentHeight
= this.documentDimensions_
.height
* zoom
;
115 // If scrollbars are required for one direction, expand the document in the
116 // other direction to take the width of the scrollbars into account when
117 // deciding whether the other direction needs scrollbars.
118 if (documentWidth
> this.window_
.innerWidth
)
119 documentHeight
+= this.scrollbarWidth_
;
120 else if (documentHeight
> this.window_
.innerHeight
)
121 documentWidth
+= this.scrollbarWidth_
;
123 horizontal
: documentWidth
> this.window_
.innerWidth
,
124 vertical
: documentHeight
> this.window_
.innerHeight
129 * Returns true if the document needs scrollbars at the current zoom level.
130 * @return {Object} with 'x' and 'y' keys which map to bool values
131 * indicating if the horizontal and vertical scrollbars are needed
134 documentHasScrollbars: function() {
135 return this.documentNeedsScrollbars_(this.zoom_
);
140 * Helper function called when the zoomed document size changes.
142 contentSizeChanged_: function() {
143 if (this.documentDimensions_
) {
144 this.sizer_
.style
.width
=
145 this.documentDimensions_
.width
* this.zoom_
+ 'px';
146 this.sizer_
.style
.height
= this.documentDimensions_
.height
* this.zoom_
+
147 this.topToolbarHeight_
+ 'px';
153 * Called when the viewport should be updated.
155 updateViewport_: function() {
156 this.viewportChangedCallback_();
161 * Called when the viewport size changes.
163 resize_: function() {
164 if (this.fittingType_
== Viewport
.FittingType
.FIT_TO_PAGE
)
165 this.fitToPageInternal_(false);
166 else if (this.fittingType_
== Viewport
.FittingType
.FIT_TO_WIDTH
)
169 this.updateViewport_();
173 * @type {Object} the scroll position of the viewport.
177 x
: this.window_
.pageXOffset
,
178 y
: this.window_
.pageYOffset
- this.topToolbarHeight_
183 * Scroll the viewport to the specified position.
184 * @type {Object} position the position to scroll to.
186 set position(position
) {
187 this.window_
.scrollTo(position
.x
, position
.y
+ this.topToolbarHeight_
);
191 * @type {Object} the size of the viewport excluding scrollbars.
194 var needsScrollbars
= this.documentNeedsScrollbars_(this.zoom_
);
195 var scrollbarWidth
= needsScrollbars
.vertical
? this.scrollbarWidth_
: 0;
196 var scrollbarHeight
= needsScrollbars
.horizontal
? this.scrollbarWidth_
: 0;
198 width
: this.window_
.innerWidth
- scrollbarWidth
,
199 height
: this.window_
.innerHeight
- scrollbarHeight
204 * @type {number} the zoom level of the viewport.
212 * Used to wrap a function that might perform zooming on the viewport. This is
213 * required so that we can notify the plugin that zooming is in progress
214 * so that while zooming is taking place it can stop reacting to scroll events
215 * from the viewport. This is to avoid flickering.
217 mightZoom_: function(f
) {
218 this.beforeZoomCallback_();
219 this.allowedToChangeZoom_
= true;
221 this.allowedToChangeZoom_
= false;
222 this.afterZoomCallback_();
227 * Sets the zoom of the viewport.
228 * @param {number} newZoom the zoom level to zoom to.
230 setZoomInternal_: function(newZoom
) {
231 if (!this.allowedToChangeZoom_
) {
232 throw 'Called Viewport.setZoomInternal_ without calling ' +
233 'Viewport.mightZoom_.';
235 // Record the scroll position (relative to the top-left of the window).
236 var currentScrollPos
= {
237 x
: this.position
.x
/ this.zoom_
,
238 y
: this.position
.y
/ this.zoom_
240 this.zoom_
= newZoom
;
241 this.contentSizeChanged_();
242 // Scroll to the scaled scroll position.
244 x
: currentScrollPos
.x
* newZoom
,
245 y
: currentScrollPos
.y
* newZoom
250 * Sets the zoom to the given zoom level.
251 * @param {number} newZoom the zoom level to zoom to.
253 setZoom: function(newZoom
) {
254 this.fittingType_
= Viewport
.FittingType
.NONE
;
255 newZoom
= Math
.max(Viewport
.ZOOM_FACTOR_RANGE
.min
,
256 Math
.min(newZoom
, Viewport
.ZOOM_FACTOR_RANGE
.max
));
257 this.mightZoom_(function() {
258 this.setZoomInternal_(newZoom
);
259 this.updateViewport_();
264 * @type {number} the width of scrollbars in the viewport in pixels.
266 get scrollbarWidth() {
267 return this.scrollbarWidth_
;
271 * @type {Viewport.FittingType} the fitting type the viewport is currently in.
274 return this.fittingType_
;
279 * @param {integer} y the y-coordinate to get the page at.
280 * @return {integer} the index of a page overlapping the given y-coordinate.
282 getPageAtY_: function(y
) {
284 var max
= this.pageDimensions_
.length
- 1;
286 var page
= Math
.floor(min
+ ((max
- min
) / 2));
287 // There might be a gap between the pages, in which case use the bottom
288 // of the previous page as the top for finding the page.
291 top
= this.pageDimensions_
[page
- 1].y
+
292 this.pageDimensions_
[page
- 1].height
;
294 var bottom
= this.pageDimensions_
[page
].y
+
295 this.pageDimensions_
[page
].height
;
297 if (top
<= y
&& bottom
> y
)
308 * Returns the page with the greatest proportion of its height in the current
310 * @return {int} the index of the most visible page.
312 getMostVisiblePage: function() {
313 var firstVisiblePage
= this.getPageAtY_(this.position
.y
/ this.zoom_
);
314 if (firstVisiblePage
== this.pageDimensions_
.length
- 1)
315 return firstVisiblePage
;
318 x
: this.position
.x
/ this.zoom_
,
319 y
: this.position
.y
/ this.zoom_
,
320 width
: this.size
.width
/ this.zoom_
,
321 height
: this.size
.height
/ this.zoom_
323 var firstVisiblePageVisibility
= getIntersectionHeight(
324 this.pageDimensions_
[firstVisiblePage
], viewportRect
) /
325 this.pageDimensions_
[firstVisiblePage
].height
;
326 var nextPageVisibility
= getIntersectionHeight(
327 this.pageDimensions_
[firstVisiblePage
+ 1], viewportRect
) /
328 this.pageDimensions_
[firstVisiblePage
+ 1].height
;
329 if (nextPageVisibility
> firstVisiblePageVisibility
)
330 return firstVisiblePage
+ 1;
331 return firstVisiblePage
;
336 * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is
337 * the dimensions for a given page and if |widthOnly| is true, it indicates
338 * that fit-to-page zoom should be computed rather than fit-to-page.
339 * @param {Object} pageDimensions the dimensions of a given page
340 * @param {boolean} widthOnly a bool indicating whether fit-to-page or
341 * fit-to-width should be computed.
342 * @return {number} the zoom to use
344 computeFittingZoom_: function(pageDimensions
, widthOnly
) {
345 // First compute the zoom without scrollbars.
346 var zoomWidth
= this.window_
.innerWidth
/ pageDimensions
.width
;
352 zoomHeight
= this.window_
.innerHeight
/ pageDimensions
.height
;
353 zoom
= Math
.min(zoomWidth
, zoomHeight
);
355 // Check if there needs to be any scrollbars.
356 var needsScrollbars
= this.documentNeedsScrollbars_(zoom
);
358 // If the document fits, just return the zoom.
359 if (!needsScrollbars
.horizontal
&& !needsScrollbars
.vertical
)
362 var zoomedDimensions
= {
363 width
: this.documentDimensions_
.width
* zoom
,
364 height
: this.documentDimensions_
.height
* zoom
367 // Check if adding a scrollbar will result in needing the other scrollbar.
368 var scrollbarWidth
= this.scrollbarWidth_
;
369 if (needsScrollbars
.horizontal
&&
370 zoomedDimensions
.height
> this.window_
.innerHeight
- scrollbarWidth
) {
371 needsScrollbars
.vertical
= true;
373 if (needsScrollbars
.vertical
&&
374 zoomedDimensions
.width
> this.window_
.innerWidth
- scrollbarWidth
) {
375 needsScrollbars
.horizontal
= true;
378 // Compute available window space.
379 var windowWithScrollbars
= {
380 width
: this.window_
.innerWidth
,
381 height
: this.window_
.innerHeight
383 if (needsScrollbars
.horizontal
)
384 windowWithScrollbars
.height
-= scrollbarWidth
;
385 if (needsScrollbars
.vertical
)
386 windowWithScrollbars
.width
-= scrollbarWidth
;
388 // Recompute the zoom.
389 zoomWidth
= windowWithScrollbars
.width
/ pageDimensions
.width
;
393 zoomHeight
= windowWithScrollbars
.height
/ pageDimensions
.height
;
394 zoom
= Math
.min(zoomWidth
, zoomHeight
);
400 * Zoom the viewport so that the page-width consumes the entire viewport.
402 fitToWidth: function() {
403 this.mightZoom_(function() {
404 this.fittingType_
= Viewport
.FittingType
.FIT_TO_WIDTH
;
405 if (!this.documentDimensions_
)
407 // When computing fit-to-width, the maximum width of a page in the
408 // document is used, which is equal to the size of the document width.
409 this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_
,
411 var page
= this.getMostVisiblePage();
412 this.updateViewport_();
418 * Zoom the viewport so that a page consumes the entire viewport.
419 * @param {boolean} scrollToTopOfPage Set to true if the viewport should be
420 * scrolled to the top of the current page. Set to false if the viewport
421 * should remain at the current scroll position.
423 fitToPageInternal_: function(scrollToTopOfPage
) {
424 this.mightZoom_(function() {
425 this.fittingType_
= Viewport
.FittingType
.FIT_TO_PAGE
;
426 if (!this.documentDimensions_
)
428 var page
= this.getMostVisiblePage();
429 // Fit to the current page's height and the widest page's width.
431 width
: this.documentDimensions_
.width
,
432 height
: this.pageDimensions_
[page
].height
,
434 this.setZoomInternal_(this.computeFittingZoom_(dimensions
, false));
435 if (scrollToTopOfPage
) {
438 y
: this.pageDimensions_
[page
].y
* this.zoom_
441 this.updateViewport_();
446 * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
447 * the viewport to the top of the current page.
449 fitToPage: function() {
450 this.fitToPageInternal_(true);
454 * Zoom out to the next predefined zoom level.
456 zoomOut: function() {
457 this.mightZoom_(function() {
458 this.fittingType_
= Viewport
.FittingType
.NONE
;
459 var nextZoom
= Viewport
.ZOOM_FACTORS
[0];
460 for (var i
= 0; i
< Viewport
.ZOOM_FACTORS
.length
; i
++) {
461 if (Viewport
.ZOOM_FACTORS
[i
] < this.zoom_
)
462 nextZoom
= Viewport
.ZOOM_FACTORS
[i
];
464 this.setZoomInternal_(nextZoom
);
465 this.updateViewport_();
470 * Zoom in to the next predefined zoom level.
473 this.mightZoom_(function() {
474 this.fittingType_
= Viewport
.FittingType
.NONE
;
475 var nextZoom
= Viewport
.ZOOM_FACTORS
[Viewport
.ZOOM_FACTORS
.length
- 1];
476 for (var i
= Viewport
.ZOOM_FACTORS
.length
- 1; i
>= 0; i
--) {
477 if (Viewport
.ZOOM_FACTORS
[i
] > this.zoom_
)
478 nextZoom
= Viewport
.ZOOM_FACTORS
[i
];
480 this.setZoomInternal_(nextZoom
);
481 this.updateViewport_();
486 * Go to the given page index.
487 * @param {number} page the index of the page to go to. zero-based.
489 goToPage: function(page
) {
490 this.mightZoom_(function() {
491 if (this.pageDimensions_
.length
=== 0)
495 if (page
>= this.pageDimensions_
.length
)
496 page
= this.pageDimensions_
.length
- 1;
497 var dimensions
= this.pageDimensions_
[page
];
498 var toolbarOffset
= 0;
499 // Unless we're in fit to page mode, scroll above the page by
500 // |this.topToolbarHeight_| so that the toolbar isn't covering it
502 if (this.fittingType_
!= Viewport
.FittingType
.FIT_TO_PAGE
)
503 toolbarOffset
= this.topToolbarHeight_
;
505 x
: dimensions
.x
* this.zoom_
,
506 y
: dimensions
.y
* this.zoom_
- toolbarOffset
508 this.updateViewport_();
513 * Set the dimensions of the document.
514 * @param {Object} documentDimensions the dimensions of the document
516 setDocumentDimensions: function(documentDimensions
) {
517 this.mightZoom_(function() {
518 var initialDimensions
= !this.documentDimensions_
;
519 this.documentDimensions_
= documentDimensions
;
520 this.pageDimensions_
= this.documentDimensions_
.pageDimensions
;
521 if (initialDimensions
) {
522 this.setZoomInternal_(
523 Math
.min(this.defaultZoom_
,
524 this.computeFittingZoom_(this.documentDimensions_
, true)));
527 y
: -this.topToolbarHeight_
530 this.contentSizeChanged_();
536 * Get the coordinates of the page contents (excluding the page shadow)
537 * relative to the screen.
538 * @param {number} page the index of the page to get the rect for.
539 * @return {Object} a rect representing the page in screen coordinates.
541 getPageScreenRect: function(page
) {
542 if (!this.documentDimensions_
) {
550 if (page
>= this.pageDimensions_
.length
)
551 page
= this.pageDimensions_
.length
- 1;
553 var pageDimensions
= this.pageDimensions_
[page
];
555 // Compute the page dimensions minus the shadows.
556 var insetDimensions
= {
557 x
: pageDimensions
.x
+ Viewport
.PAGE_SHADOW
.left
,
558 y
: pageDimensions
.y
+ Viewport
.PAGE_SHADOW
.top
,
559 width
: pageDimensions
.width
- Viewport
.PAGE_SHADOW
.left
-
560 Viewport
.PAGE_SHADOW
.right
,
561 height
: pageDimensions
.height
- Viewport
.PAGE_SHADOW
.top
-
562 Viewport
.PAGE_SHADOW
.bottom
565 // Compute the x-coordinate of the page within the document.
566 // TODO(raymes): This should really be set when the PDF plugin passes the
567 // page coordinates, but it isn't yet.
568 var x
= (this.documentDimensions_
.width
- pageDimensions
.width
) / 2 +
569 Viewport
.PAGE_SHADOW
.left
;
570 // Compute the space on the left of the document if the document fits
571 // completely in the screen.
572 var spaceOnLeft
= (this.size
.width
-
573 this.documentDimensions_
.width
* this.zoom_
) / 2;
574 spaceOnLeft
= Math
.max(spaceOnLeft
, 0);
577 x
: x
* this.zoom_
+ spaceOnLeft
- this.window_
.pageXOffset
,
578 y
: insetDimensions
.y
* this.zoom_
- this.window_
.pageYOffset
,
579 width
: insetDimensions
.width
* this.zoom_
,
580 height
: insetDimensions
.height
* this.zoom_