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.
19 * @param {Window} window the window
20 * @param {Object} sizer is the element which represents the size of the
21 * document in the viewport
22 * @param {Function} viewportChangedCallback is run when the viewport changes
23 * @param {Function} beforeZoomCallback is run before a change in zoom
24 * @param {Function} afterZoomCallback is run after a change in zoom
25 * @param {number} scrollbarWidth the width of scrollbars on the page
27 function Viewport(window
,
29 viewportChangedCallback
,
33 this.window_
= window
;
35 this.viewportChangedCallback_
= viewportChangedCallback
;
36 this.beforeZoomCallback_
= beforeZoomCallback
;
37 this.afterZoomCallback_
= afterZoomCallback
;
38 this.allowedToChangeZoom_
= false;
40 this.documentDimensions_
= null;
41 this.pageDimensions_
= [];
42 this.scrollbarWidth_
= scrollbarWidth
;
43 this.fittingType_
= Viewport
.FittingType
.NONE
;
45 window
.addEventListener('scroll', this.updateViewport_
.bind(this));
46 window
.addEventListener('resize', this.resize_
.bind(this));
50 * Enumeration of page fitting types.
53 Viewport
.FittingType
= {
55 FIT_TO_PAGE
: 'fit-to-page',
56 FIT_TO_WIDTH
: 'fit-to-width'
60 * The increment to scroll a page by in pixels when up/down/left/right arrow
61 * keys are pressed. Usually we just let the browser handle scrolling on the
62 * window when these keys are pressed but in certain cases we need to simulate
65 Viewport
.SCROLL_INCREMENT
= 40;
68 * Predefined zoom factors to be used when zooming in/out. These are in
69 * ascending order. This should match the list in
70 * chrome/browser/chrome_page_zoom_constants.cc.
72 Viewport
.ZOOM_FACTORS
= [0.25, 0.333, 0.5, 0.666, 0.75, 0.9, 1,
73 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
76 * The minimum and maximum range to be used to clip zoom factor.
78 Viewport
.ZOOM_FACTOR_RANGE
= {
79 min
: Viewport
.ZOOM_FACTORS
[0],
80 max
: Viewport
.ZOOM_FACTORS
[Viewport
.ZOOM_FACTORS
.length
- 1]
84 * The width of the page shadow around pages in pixels.
86 Viewport
.PAGE_SHADOW
= {top
: 3, bottom
: 7, left
: 5, right
: 5};
88 Viewport
.prototype = {
91 * Returns true if the document needs scrollbars at the given zoom level.
92 * @param {number} zoom compute whether scrollbars are needed at this zoom
93 * @return {Object} with 'horizontal' and 'vertical' keys which map to bool
94 * values indicating if the horizontal and vertical scrollbars are needed
97 documentNeedsScrollbars_: function(zoom
) {
98 if (!this.documentDimensions_
) {
104 var documentWidth
= this.documentDimensions_
.width
* zoom
;
105 var documentHeight
= this.documentDimensions_
.height
* zoom
;
107 // If scrollbars are required for one direction, expand the document in the
108 // other direction to take the width of the scrollbars into account when
109 // deciding whether the other direction needs scrollbars.
110 if (documentWidth
> this.window_
.innerWidth
)
111 documentHeight
+= this.scrollbarWidth_
;
112 else if (documentHeight
> this.window_
.innerHeight
)
113 documentWidth
+= this.scrollbarWidth_
;
115 horizontal
: documentWidth
> this.window_
.innerWidth
,
116 vertical
: documentHeight
> this.window_
.innerHeight
121 * Returns true if the document needs scrollbars at the current zoom level.
122 * @return {Object} with 'x' and 'y' keys which map to bool values
123 * indicating if the horizontal and vertical scrollbars are needed
126 documentHasScrollbars: function() {
127 return this.documentNeedsScrollbars_(this.zoom_
);
132 * Helper function called when the zoomed document size changes.
134 contentSizeChanged_: function() {
135 if (this.documentDimensions_
) {
136 this.sizer_
.style
.width
=
137 this.documentDimensions_
.width
* this.zoom_
+ 'px';
138 this.sizer_
.style
.height
=
139 this.documentDimensions_
.height
* this.zoom_
+ 'px';
145 * Called when the viewport should be updated.
147 updateViewport_: function() {
148 this.viewportChangedCallback_();
153 * Called when the viewport size changes.
155 resize_: function() {
156 if (this.fittingType_
== Viewport
.FittingType
.FIT_TO_PAGE
)
158 else if (this.fittingType_
== Viewport
.FittingType
.FIT_TO_WIDTH
)
161 this.updateViewport_();
165 * @type {Object} the scroll position of the viewport.
169 x
: this.window_
.pageXOffset
,
170 y
: this.window_
.pageYOffset
175 * Scroll the viewport to the specified position.
176 * @type {Object} position the position to scroll to.
178 set position(position
) {
179 this.window_
.scrollTo(position
.x
, position
.y
);
183 * @type {Object} the size of the viewport excluding scrollbars.
186 var needsScrollbars
= this.documentNeedsScrollbars_(this.zoom_
);
187 var scrollbarWidth
= needsScrollbars
.vertical
? this.scrollbarWidth_
: 0;
188 var scrollbarHeight
= needsScrollbars
.horizontal
? this.scrollbarWidth_
: 0;
190 width
: this.window_
.innerWidth
- scrollbarWidth
,
191 height
: this.window_
.innerHeight
- scrollbarHeight
196 * @type {number} the zoom level of the viewport.
204 * Used to wrap a function that might perform zooming on the viewport. This is
205 * required so that we can notify the plugin that zooming is in progress
206 * so that while zooming is taking place it can stop reacting to scroll events
207 * from the viewport. This is to avoid flickering.
209 mightZoom_: function(f
) {
210 this.beforeZoomCallback_();
211 this.allowedToChangeZoom_
= true;
213 this.allowedToChangeZoom_
= false;
214 this.afterZoomCallback_();
219 * Sets the zoom of the viewport.
220 * @param {number} newZoom the zoom level to zoom to.
222 setZoomInternal_: function(newZoom
) {
223 if (!this.allowedToChangeZoom_
) {
224 throw 'Called Viewport.setZoomInternal_ without calling ' +
225 'Viewport.mightZoom_.';
227 // Record the scroll position (relative to the top-left of the window).
228 var currentScrollPos
= [
229 this.window_
.pageXOffset
/ this.zoom_
,
230 this.window_
.pageYOffset
/ this.zoom_
232 this.zoom_
= newZoom
;
233 this.contentSizeChanged_();
234 // Scroll to the scaled scroll position.
235 this.window_
.scrollTo(currentScrollPos
[0] * newZoom
,
236 currentScrollPos
[1] * newZoom
);
240 * Sets the zoom to the given zoom level.
241 * @param {number} newZoom the zoom level to zoom to.
243 setZoom: function(newZoom
) {
244 newZoom
= Math
.max(Viewport
.ZOOM_FACTOR_RANGE
.min
,
245 Math
.min(newZoom
, Viewport
.ZOOM_FACTOR_RANGE
.max
));
246 this.mightZoom_(function() {
247 this.setZoomInternal_(newZoom
);
248 this.updateViewport_();
253 * @type {number} the width of scrollbars in the viewport in pixels.
255 get scrollbarWidth() {
256 return this.scrollbarWidth_
;
260 * @type {Viewport.FittingType} the fitting type the viewport is currently in.
263 return this.fittingType_
;
268 * @param {integer} y the y-coordinate to get the page at.
269 * @return {integer} the index of a page overlapping the given y-coordinate.
271 getPageAtY_: function(y
) {
273 var max
= this.pageDimensions_
.length
- 1;
275 var page
= Math
.floor(min
+ ((max
- min
) / 2));
276 // There might be a gap between the pages, in which case use the bottom
277 // of the previous page as the top for finding the page.
280 top
= this.pageDimensions_
[page
- 1].y
+
281 this.pageDimensions_
[page
- 1].height
;
283 var bottom
= this.pageDimensions_
[page
].y
+
284 this.pageDimensions_
[page
].height
;
286 if (top
<= y
&& bottom
> y
)
297 * Returns the page with the greatest proportion of its height in the current
299 * @return {int} the index of the most visible page.
301 getMostVisiblePage: function() {
302 var firstVisiblePage
= this.getPageAtY_(this.position
.y
/ this.zoom_
);
303 if (firstVisiblePage
== this.pageDimensions_
.length
- 1)
304 return firstVisiblePage
;
307 x
: this.position
.x
/ this.zoom_
,
308 y
: this.position
.y
/ this.zoom_
,
309 width
: this.size
.width
/ this.zoom_
,
310 height
: this.size
.height
/ this.zoom_
312 var firstVisiblePageVisibility
= getIntersectionHeight(
313 this.pageDimensions_
[firstVisiblePage
], viewportRect
) /
314 this.pageDimensions_
[firstVisiblePage
].height
;
315 var nextPageVisibility
= getIntersectionHeight(
316 this.pageDimensions_
[firstVisiblePage
+ 1], viewportRect
) /
317 this.pageDimensions_
[firstVisiblePage
+ 1].height
;
318 if (nextPageVisibility
> firstVisiblePageVisibility
)
319 return firstVisiblePage
+ 1;
320 return firstVisiblePage
;
325 * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is
326 * the dimensions for a given page and if |widthOnly| is true, it indicates
327 * that fit-to-page zoom should be computed rather than fit-to-page.
328 * @param {Object} pageDimensions the dimensions of a given page
329 * @param {boolean} widthOnly a bool indicating whether fit-to-page or
330 * fit-to-width should be computed.
331 * @return {number} the zoom to use
333 computeFittingZoom_: function(pageDimensions
, widthOnly
) {
334 // First compute the zoom without scrollbars.
335 var zoomWidth
= this.window_
.innerWidth
/ pageDimensions
.width
;
340 var zoomHeight
= this.window_
.innerHeight
/ pageDimensions
.height
;
341 zoom
= Math
.min(zoomWidth
, zoomHeight
);
343 // Check if there needs to be any scrollbars.
344 var needsScrollbars
= this.documentNeedsScrollbars_(zoom
);
346 // If the document fits, just return the zoom.
347 if (!needsScrollbars
.horizontal
&& !needsScrollbars
.vertical
)
350 var zoomedDimensions
= {
351 width
: this.documentDimensions_
.width
* zoom
,
352 height
: this.documentDimensions_
.height
* zoom
355 // Check if adding a scrollbar will result in needing the other scrollbar.
356 var scrollbarWidth
= this.scrollbarWidth_
;
357 if (needsScrollbars
.horizontal
&&
358 zoomedDimensions
.height
> this.window_
.innerHeight
- scrollbarWidth
) {
359 needsScrollbars
.vertical
= true;
361 if (needsScrollbars
.vertical
&&
362 zoomedDimensions
.width
> this.window_
.innerWidth
- scrollbarWidth
) {
363 needsScrollbars
.horizontal
= true;
366 // Compute available window space.
367 var windowWithScrollbars
= {
368 width
: this.window_
.innerWidth
,
369 height
: this.window_
.innerHeight
371 if (needsScrollbars
.horizontal
)
372 windowWithScrollbars
.height
-= scrollbarWidth
;
373 if (needsScrollbars
.vertical
)
374 windowWithScrollbars
.width
-= scrollbarWidth
;
376 // Recompute the zoom.
377 zoomWidth
= windowWithScrollbars
.width
/ pageDimensions
.width
;
381 var zoomHeight
= windowWithScrollbars
.height
/ pageDimensions
.height
;
382 zoom
= Math
.min(zoomWidth
, zoomHeight
);
388 * Zoom the viewport so that the page-width consumes the entire viewport.
390 fitToWidth: function() {
391 this.mightZoom_(function() {
392 this.fittingType_
= Viewport
.FittingType
.FIT_TO_WIDTH
;
393 if (!this.documentDimensions_
)
395 // Track the last y-position to stay at the same position after zooming.
396 var oldY
= this.window_
.pageYOffset
/ this.zoom_
;
397 // When computing fit-to-width, the maximum width of a page in the
398 // document is used, which is equal to the size of the document width.
399 this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_
,
401 var page
= this.getMostVisiblePage();
402 this.window_
.scrollTo(0, oldY
* this.zoom_
);
403 this.updateViewport_();
408 * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
409 * to the top of the most visible page.
411 fitToPage: function() {
412 this.mightZoom_(function() {
413 this.fittingType_
= Viewport
.FittingType
.FIT_TO_PAGE
;
414 if (!this.documentDimensions_
)
416 var page
= this.getMostVisiblePage();
417 // Fit to the current page's height and the widest page's width.
419 width
: this.documentDimensions_
.width
,
420 height
: this.pageDimensions_
[page
].height
,
422 this.setZoomInternal_(this.computeFittingZoom_(dimensions
, false));
423 this.window_
.scrollTo(0, this.pageDimensions_
[page
].y
* this.zoom_
);
424 this.updateViewport_();
429 * Zoom out to the next predefined zoom level.
431 zoomOut: function() {
432 this.mightZoom_(function() {
433 this.fittingType_
= Viewport
.FittingType
.NONE
;
434 var nextZoom
= Viewport
.ZOOM_FACTORS
[0];
435 for (var i
= 0; i
< Viewport
.ZOOM_FACTORS
.length
; i
++) {
436 if (Viewport
.ZOOM_FACTORS
[i
] < this.zoom_
)
437 nextZoom
= Viewport
.ZOOM_FACTORS
[i
];
439 this.setZoomInternal_(nextZoom
);
440 this.updateViewport_();
445 * Zoom in to the next predefined zoom level.
448 this.mightZoom_(function() {
449 this.fittingType_
= Viewport
.FittingType
.NONE
;
450 var nextZoom
= Viewport
.ZOOM_FACTORS
[Viewport
.ZOOM_FACTORS
.length
- 1];
451 for (var i
= Viewport
.ZOOM_FACTORS
.length
- 1; i
>= 0; i
--) {
452 if (Viewport
.ZOOM_FACTORS
[i
] > this.zoom_
)
453 nextZoom
= Viewport
.ZOOM_FACTORS
[i
];
455 this.setZoomInternal_(nextZoom
);
456 this.updateViewport_();
461 * Go to the given page index.
462 * @param {number} page the index of the page to go to. zero-based.
464 goToPage: function(page
) {
465 this.mightZoom_(function() {
466 if (this.pageDimensions_
.length
== 0)
470 if (page
>= this.pageDimensions_
.length
)
471 page
= this.pageDimensions_
.length
- 1;
472 var dimensions
= this.pageDimensions_
[page
];
473 this.window_
.scrollTo(dimensions
.x
* this.zoom_
,
474 dimensions
.y
* this.zoom_
);
475 this.updateViewport_();
480 * Set the dimensions of the document.
481 * @param {Object} documentDimensions the dimensions of the document
483 setDocumentDimensions: function(documentDimensions
) {
484 this.mightZoom_(function() {
485 var initialDimensions
= !this.documentDimensions_
;
486 this.documentDimensions_
= documentDimensions
;
487 this.pageDimensions_
= this.documentDimensions_
.pageDimensions
;
488 if (initialDimensions
) {
489 this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_
,
492 this.setZoomInternal_(1);
493 this.window_
.scrollTo(0, 0);
495 this.contentSizeChanged_();
501 * Get the coordinates of the page contents (excluding the page shadow)
502 * relative to the screen.
503 * @param {number} page the index of the page to get the rect for.
504 * @return {Object} a rect representing the page in screen coordinates.
506 getPageScreenRect: function(page
) {
507 if (!this.documentDimensions_
) {
515 if (page
>= this.pageDimensions_
.length
)
516 page
= this.pageDimensions_
.length
- 1;
518 var pageDimensions
= this.pageDimensions_
[page
];
520 // Compute the page dimensions minus the shadows.
521 var insetDimensions
= {
522 x
: pageDimensions
.x
+ Viewport
.PAGE_SHADOW
.left
,
523 y
: pageDimensions
.y
+ Viewport
.PAGE_SHADOW
.top
,
524 width
: pageDimensions
.width
- Viewport
.PAGE_SHADOW
.left
-
525 Viewport
.PAGE_SHADOW
.right
,
526 height
: pageDimensions
.height
- Viewport
.PAGE_SHADOW
.top
-
527 Viewport
.PAGE_SHADOW
.bottom
530 // Compute the x-coordinate of the page within the document.
531 // TODO(raymes): This should really be set when the PDF plugin passes the
532 // page coordinates, but it isn't yet.
533 var x
= (this.documentDimensions_
.width
- pageDimensions
.width
) / 2 +
534 Viewport
.PAGE_SHADOW
.left
;
535 // Compute the space on the left of the document if the document fits
536 // completely in the screen.
537 var spaceOnLeft
= (this.size
.width
-
538 this.documentDimensions_
.width
* this.zoom_
) / 2;
539 spaceOnLeft
= Math
.max(spaceOnLeft
, 0);
542 x
: x
* this.zoom_
+ spaceOnLeft
- this.window_
.pageXOffset
,
543 y
: insetDimensions
.y
* this.zoom_
- this.window_
.pageYOffset
,
544 width
: insetDimensions
.width
* this.zoom_
,
545 height
: insetDimensions
.height
* this.zoom_