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 area of the intersection of two rectangles.
7 * @param {Object} rect1 the first rect
8 * @param {Object} rect2 the second rect
9 * @return {number} the area of the intersection of the rects
11 function getIntersectionArea(rect1, rect2) {
12 var xOverlap = Math.max(0,
13 Math.min(rect1.x + rect1.width, rect2.x + rect2.width) -
14 Math.max(rect1.x, rect2.x));
15 var yOverlap = Math.max(0,
16 Math.min(rect1.y + rect1.height, rect2.y + rect2.height) -
17 Math.max(rect1.y, rect2.y));
18 return xOverlap * yOverlap;
22 * Create a new viewport.
23 * @param {Window} window the window
24 * @param {Object} sizer is the element which represents the size of the
25 * document in the viewport
26 * @param {Function} viewportChangedCallback is run when the viewport changes
27 * @param {Function} beforeZoomCallback is run before a change in zoom
28 * @param {Function} afterZoomCallback is run after a change in zoom
29 * @param {number} scrollbarWidth the width of scrollbars on the page
31 function Viewport(window,
33 viewportChangedCallback,
37 this.window_ = window;
39 this.viewportChangedCallback_ = viewportChangedCallback;
40 this.beforeZoomCallback_ = beforeZoomCallback;
41 this.afterZoomCallback_ = afterZoomCallback;
42 this.allowedToChangeZoom_ = false;
44 this.documentDimensions_ = null;
45 this.pageDimensions_ = [];
46 this.scrollbarWidth_ = scrollbarWidth;
47 this.fittingType_ = Viewport.FittingType.NONE;
49 window.addEventListener('scroll', this.updateViewport_.bind(this));
50 window.addEventListener('resize', this.resize_.bind(this));
54 * Enumeration of page fitting types.
57 Viewport.FittingType = {
59 FIT_TO_PAGE: 'fit-to-page',
60 FIT_TO_WIDTH: 'fit-to-width'
64 * The increment to scroll a page by in pixels when up/down/left/right arrow
65 * keys are pressed. Usually we just let the browser handle scrolling on the
66 * window when these keys are pressed but in certain cases we need to simulate
69 Viewport.SCROLL_INCREMENT = 40;
72 * Predefined zoom factors to be used when zooming in/out. These are in
73 * ascending order. This should match the list in
74 * chrome/browser/chrome_page_zoom_constants.cc.
76 Viewport.ZOOM_FACTORS = [0.25, 0.333, 0.5, 0.666, 0.75, 0.9, 1,
77 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
80 * The minimum and maximum range to be used to clip zoom factor.
82 Viewport.ZOOM_FACTOR_RANGE = {
83 min: Viewport.ZOOM_FACTORS[0],
84 max: Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1]
88 * The width of the page shadow around pages in pixels.
90 Viewport.PAGE_SHADOW = {top: 3, bottom: 7, left: 5, right: 5};
92 Viewport.prototype = {
95 * Returns true if the document needs scrollbars at the given zoom level.
96 * @param {number} zoom compute whether scrollbars are needed at this zoom
97 * @return {Object} with 'horizontal' and 'vertical' keys which map to bool
98 * values indicating if the horizontal and vertical scrollbars are needed
101 documentNeedsScrollbars_: function(zoom) {
102 if (!this.documentDimensions_) {
108 var documentWidth = this.documentDimensions_.width * zoom;
109 var documentHeight = this.documentDimensions_.height * zoom;
111 horizontal: documentWidth > this.window_.innerWidth,
112 vertical: documentHeight > this.window_.innerHeight
117 * Returns true if the document needs scrollbars at the current zoom level.
118 * @return {Object} with 'x' and 'y' keys which map to bool values
119 * indicating if the horizontal and vertical scrollbars are needed
122 documentHasScrollbars: function() {
123 return this.documentNeedsScrollbars_(this.zoom_);
128 * Helper function called when the zoomed document size changes.
130 contentSizeChanged_: function() {
131 if (this.documentDimensions_) {
132 this.sizer_.style.width =
133 this.documentDimensions_.width * this.zoom_ + 'px';
134 this.sizer_.style.height =
135 this.documentDimensions_.height * this.zoom_ + 'px';
141 * Called when the viewport should be updated.
143 updateViewport_: function() {
144 this.viewportChangedCallback_();
149 * Called when the viewport size changes.
151 resize_: function() {
152 if (this.fittingType_ == Viewport.FittingType.FIT_TO_PAGE)
154 else if (this.fittingType_ == Viewport.FittingType.FIT_TO_WIDTH)
157 this.updateViewport_();
161 * @type {Object} the scroll position of the viewport.
165 x: this.window_.pageXOffset,
166 y: this.window_.pageYOffset
171 * Scroll the viewport to the specified position.
172 * @type {Object} position the position to scroll to.
174 set position(position) {
175 this.window_.scrollTo(position.x, position.y);
179 * @type {Object} the size of the viewport excluding scrollbars.
182 var needsScrollbars = this.documentNeedsScrollbars_(this.zoom_);
183 var scrollbarWidth = needsScrollbars.vertical ? this.scrollbarWidth_ : 0;
184 var scrollbarHeight = needsScrollbars.horizontal ? this.scrollbarWidth_ : 0;
186 width: this.window_.innerWidth - scrollbarWidth,
187 height: this.window_.innerHeight - scrollbarHeight
192 * @type {number} the zoom level of the viewport.
200 * Used to wrap a function that might perform zooming on the viewport. This is
201 * required so that we can notify the plugin that zooming is in progress
202 * so that while zooming is taking place it can stop reacting to scroll events
203 * from the viewport. This is to avoid flickering.
205 mightZoom_: function(f) {
206 this.beforeZoomCallback_();
207 this.allowedToChangeZoom_ = true;
209 this.allowedToChangeZoom_ = false;
210 this.afterZoomCallback_();
215 * Sets the zoom of the viewport.
216 * @param {number} newZoom the zoom level to zoom to.
218 setZoomInternal_: function(newZoom) {
219 if (!this.allowedToChangeZoom_) {
220 throw 'Called Viewport.setZoomInternal_ without calling ' +
221 'Viewport.mightZoom_.';
223 // Record the scroll position (relative to the top-left of the window).
224 var currentScrollPos = [
225 this.window_.pageXOffset / this.zoom_,
226 this.window_.pageYOffset / this.zoom_
228 this.zoom_ = newZoom;
229 this.contentSizeChanged_();
230 // Scroll to the scaled scroll position.
231 this.window_.scrollTo(currentScrollPos[0] * newZoom,
232 currentScrollPos[1] * newZoom);
236 * Sets the zoom to the given zoom level.
237 * @param {number} newZoom the zoom level to zoom to.
239 setZoom: function(newZoom) {
240 newZoom = Math.max(Viewport.ZOOM_FACTOR_RANGE.min,
241 Math.min(newZoom, Viewport.ZOOM_FACTOR_RANGE.max));
242 this.mightZoom_(function() {
243 this.setZoomInternal_(newZoom);
244 this.updateViewport_();
249 * @type {number} the width of scrollbars in the viewport in pixels.
251 get scrollbarWidth() {
252 return this.scrollbarWidth_;
256 * @type {Viewport.FittingType} the fitting type the viewport is currently in.
259 return this.fittingType_;
264 * @param {integer} y the y-coordinate to get the page at.
265 * @return {integer} the index of a page overlapping the given y-coordinate.
267 getPageAtY_: function(y) {
269 var max = this.pageDimensions_.length - 1;
271 var page = Math.floor(min + ((max - min) / 2));
272 // There might be a gap between the pages, in which case use the bottom
273 // of the previous page as the top for finding the page.
276 top = this.pageDimensions_[page - 1].y +
277 this.pageDimensions_[page - 1].height;
279 var bottom = this.pageDimensions_[page].y +
280 this.pageDimensions_[page].height;
282 if (top <= y && bottom > y)
293 * Returns the page with the most pixels in the current viewport.
294 * @return {int} the index of the most visible page.
296 getMostVisiblePage: function() {
297 var firstVisiblePage = this.getPageAtY_(this.position.y / this.zoom_);
298 var mostVisiblePage = {number: 0, area: 0};
300 x: this.position.x / this.zoom_,
301 y: this.position.y / this.zoom_,
302 width: this.size.width / this.zoom_,
303 height: this.size.height / this.zoom_
305 for (var i = firstVisiblePage; i < this.pageDimensions_.length; i++) {
306 var area = getIntersectionArea(this.pageDimensions_[i],
308 // If we hit a page with 0 area overlap, we must have gone past the
309 // pages visible in the viewport so we can break.
312 if (area > mostVisiblePage.area) {
313 mostVisiblePage.area = area;
314 mostVisiblePage.number = i;
317 return mostVisiblePage.number;
322 * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is
323 * the dimensions for a given page and if |widthOnly| is true, it indicates
324 * that fit-to-page zoom should be computed rather than fit-to-page.
325 * @param {Object} pageDimensions the dimensions of a given page
326 * @param {boolean} widthOnly a bool indicating whether fit-to-page or
327 * fit-to-width should be computed.
328 * @return {number} the zoom to use
330 computeFittingZoom_: function(pageDimensions, widthOnly) {
331 // First compute the zoom without scrollbars.
332 var zoomWidth = this.window_.innerWidth / pageDimensions.width;
337 var zoomHeight = this.window_.innerHeight / pageDimensions.height;
338 zoom = Math.min(zoomWidth, zoomHeight);
340 // Check if there needs to be any scrollbars.
341 var needsScrollbars = this.documentNeedsScrollbars_(zoom);
343 // If the document fits, just return the zoom.
344 if (!needsScrollbars.horizontal && !needsScrollbars.vertical)
347 var zoomedDimensions = {
348 width: this.documentDimensions_.width * zoom,
349 height: this.documentDimensions_.height * zoom
352 // Check if adding a scrollbar will result in needing the other scrollbar.
353 var scrollbarWidth = this.scrollbarWidth_;
354 if (needsScrollbars.horizontal &&
355 zoomedDimensions.height > this.window_.innerHeight - scrollbarWidth) {
356 needsScrollbars.vertical = true;
358 if (needsScrollbars.vertical &&
359 zoomedDimensions.width > this.window_.innerWidth - scrollbarWidth) {
360 needsScrollbars.horizontal = true;
363 // Compute available window space.
364 var windowWithScrollbars = {
365 width: this.window_.innerWidth,
366 height: this.window_.innerHeight
368 if (needsScrollbars.horizontal)
369 windowWithScrollbars.height -= scrollbarWidth;
370 if (needsScrollbars.vertical)
371 windowWithScrollbars.width -= scrollbarWidth;
373 // Recompute the zoom.
374 zoomWidth = windowWithScrollbars.width / pageDimensions.width;
378 var zoomHeight = windowWithScrollbars.height / pageDimensions.height;
379 zoom = Math.min(zoomWidth, zoomHeight);
385 * Zoom the viewport so that the page-width consumes the entire viewport.
387 fitToWidth: function() {
388 this.mightZoom_(function() {
389 this.fittingType_ = Viewport.FittingType.FIT_TO_WIDTH;
390 if (!this.documentDimensions_)
392 // Track the last y-position to stay at the same position after zooming.
393 var oldY = this.window_.pageYOffset / this.zoom_;
394 // When computing fit-to-width, the maximum width of a page in the
395 // document is used, which is equal to the size of the document width.
396 this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_,
398 var page = this.getMostVisiblePage();
399 this.window_.scrollTo(0, oldY * this.zoom_);
400 this.updateViewport_();
405 * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
406 * to the top of the most visible page.
408 fitToPage: function() {
409 this.mightZoom_(function() {
410 this.fittingType_ = Viewport.FittingType.FIT_TO_PAGE;
411 if (!this.documentDimensions_)
413 var page = this.getMostVisiblePage();
414 // Fit to the current page's height and the widest page's width.
416 width: this.documentDimensions_.width,
417 height: this.pageDimensions_[page].height,
419 this.setZoomInternal_(this.computeFittingZoom_(dimensions, false));
420 this.window_.scrollTo(0, this.pageDimensions_[page].y * this.zoom_);
421 this.updateViewport_();
426 * Zoom out to the next predefined zoom level.
428 zoomOut: function() {
429 this.mightZoom_(function() {
430 this.fittingType_ = Viewport.FittingType.NONE;
431 var nextZoom = Viewport.ZOOM_FACTORS[0];
432 for (var i = 0; i < Viewport.ZOOM_FACTORS.length; i++) {
433 if (Viewport.ZOOM_FACTORS[i] < this.zoom_)
434 nextZoom = Viewport.ZOOM_FACTORS[i];
436 this.setZoomInternal_(nextZoom);
437 this.updateViewport_();
442 * Zoom in to the next predefined zoom level.
445 this.mightZoom_(function() {
446 this.fittingType_ = Viewport.FittingType.NONE;
447 var nextZoom = Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1];
448 for (var i = Viewport.ZOOM_FACTORS.length - 1; i >= 0; i--) {
449 if (Viewport.ZOOM_FACTORS[i] > this.zoom_)
450 nextZoom = Viewport.ZOOM_FACTORS[i];
452 this.setZoomInternal_(nextZoom);
453 this.updateViewport_();
458 * Go to the given page index.
459 * @param {number} page the index of the page to go to. zero-based.
461 goToPage: function(page) {
462 this.mightZoom_(function() {
463 if (this.pageDimensions_.length == 0)
467 if (page >= this.pageDimensions_.length)
468 page = this.pageDimensions_.length - 1;
469 var dimensions = this.pageDimensions_[page];
470 this.window_.scrollTo(dimensions.x * this.zoom_,
471 dimensions.y * this.zoom_);
472 this.updateViewport_();
477 * Set the dimensions of the document.
478 * @param {Object} documentDimensions the dimensions of the document
480 setDocumentDimensions: function(documentDimensions) {
481 this.mightZoom_(function() {
482 var initialDimensions = !this.documentDimensions_;
483 this.documentDimensions_ = documentDimensions;
484 this.pageDimensions_ = this.documentDimensions_.pageDimensions;
485 if (initialDimensions) {
486 this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_,
489 this.setZoomInternal_(1);
490 this.window_.scrollTo(0, 0);
492 this.contentSizeChanged_();
498 * Get the coordinates of the page contents (excluding the page shadow)
499 * relative to the screen.
500 * @param {number} page the index of the page to get the rect for.
501 * @return {Object} a rect representing the page in screen coordinates.
503 getPageScreenRect: function(page) {
504 if (!this.documentDimensions_) {
512 if (page >= this.pageDimensions_.length)
513 page = this.pageDimensions_.length - 1;
515 var pageDimensions = this.pageDimensions_[page];
517 // Compute the page dimensions minus the shadows.
518 var insetDimensions = {
519 x: pageDimensions.x + Viewport.PAGE_SHADOW.left,
520 y: pageDimensions.y + Viewport.PAGE_SHADOW.top,
521 width: pageDimensions.width - Viewport.PAGE_SHADOW.left -
522 Viewport.PAGE_SHADOW.right,
523 height: pageDimensions.height - Viewport.PAGE_SHADOW.top -
524 Viewport.PAGE_SHADOW.bottom
527 // Compute the x-coordinate of the page within the document.
528 // TODO(raymes): This should really be set when the PDF plugin passes the
529 // page coordinates, but it isn't yet.
530 var x = (this.documentDimensions_.width - pageDimensions.width) / 2 +
531 Viewport.PAGE_SHADOW.left;
532 // Compute the space on the left of the document if the document fits
533 // completely in the screen.
534 var spaceOnLeft = (this.size.width -
535 this.documentDimensions_.width * this.zoom_) / 2;
536 spaceOnLeft = Math.max(spaceOnLeft, 0);
539 x: x * this.zoom_ + spaceOnLeft - this.window_.pageXOffset,
540 y: insetDimensions.y * this.zoom_ - this.window_.pageYOffset,
541 width: insetDimensions.width * this.zoom_,
542 height: insetDimensions.height * this.zoom_