Roll src/third_party/WebKit bf18a82:a9cee16 (svn 185297:185304)
[chromium-blink-merge.git] / chrome / browser / resources / pdf / viewport.js
blob6fac2f197695eeb12c21173835d54cb814dfe3a6
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.
5 /**
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) {
12 return Math.max(0,
13 Math.min(rect1.y + rect1.height, rect2.y + rect2.height) -
14 Math.max(rect1.y, rect2.y));
17 /**
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,
28 sizer,
29 viewportChangedCallback,
30 beforeZoomCallback,
31 afterZoomCallback,
32 scrollbarWidth) {
33 this.window_ = window;
34 this.sizer_ = sizer;
35 this.viewportChangedCallback_ = viewportChangedCallback;
36 this.beforeZoomCallback_ = beforeZoomCallback;
37 this.afterZoomCallback_ = afterZoomCallback;
38 this.allowedToChangeZoom_ = false;
39 this.zoom_ = 1;
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));
49 /**
50 * Enumeration of page fitting types.
51 * @enum {string}
53 Viewport.FittingType = {
54 NONE: 'none',
55 FIT_TO_PAGE: 'fit-to-page',
56 FIT_TO_WIDTH: 'fit-to-width'
59 /**
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
63 * these events.
65 Viewport.SCROLL_INCREMENT = 40;
67 /**
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];
75 /**
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]
83 /**
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 = {
89 /**
90 * @private
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
95 * respectively.
97 documentNeedsScrollbars_: function(zoom) {
98 if (!this.documentDimensions_) {
99 return {
100 horizontal: false,
101 vertical: false
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_;
114 return {
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
124 * respectively.
126 documentHasScrollbars: function() {
127 return this.documentNeedsScrollbars_(this.zoom_);
131 * @private
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';
144 * @private
145 * Called when the viewport should be updated.
147 updateViewport_: function() {
148 this.viewportChangedCallback_();
152 * @private
153 * Called when the viewport size changes.
155 resize_: function() {
156 if (this.fittingType_ == Viewport.FittingType.FIT_TO_PAGE)
157 this.fitToPage();
158 else if (this.fittingType_ == Viewport.FittingType.FIT_TO_WIDTH)
159 this.fitToWidth();
160 else
161 this.updateViewport_();
165 * @type {Object} the scroll position of the viewport.
167 get position() {
168 return {
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.
185 get size() {
186 var needsScrollbars = this.documentNeedsScrollbars_(this.zoom_);
187 var scrollbarWidth = needsScrollbars.vertical ? this.scrollbarWidth_ : 0;
188 var scrollbarHeight = needsScrollbars.horizontal ? this.scrollbarWidth_ : 0;
189 return {
190 width: this.window_.innerWidth - scrollbarWidth,
191 height: this.window_.innerHeight - scrollbarHeight
196 * @type {number} the zoom level of the viewport.
198 get zoom() {
199 return this.zoom_;
203 * @private
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;
212 f();
213 this.allowedToChangeZoom_ = false;
214 this.afterZoomCallback_();
218 * @private
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_();
249 }.bind(this));
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.
262 get fittingType() {
263 return this.fittingType_;
267 * @private
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) {
272 var min = 0;
273 var max = this.pageDimensions_.length - 1;
274 while (max >= min) {
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.
278 var top = 0;
279 if (page > 0) {
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)
287 return page;
288 else if (top > y)
289 max = page - 1;
290 else
291 min = page + 1;
293 return 0;
297 * Returns the page with the greatest proportion of its height in the current
298 * viewport.
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;
306 var viewportRect = {
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;
324 * @private
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;
336 var zoom;
337 if (widthOnly) {
338 zoom = zoomWidth;
339 } else {
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)
348 return zoom;
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;
378 if (widthOnly) {
379 zoom = zoomWidth;
380 } else {
381 var zoomHeight = windowWithScrollbars.height / pageDimensions.height;
382 zoom = Math.min(zoomWidth, zoomHeight);
384 return zoom;
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_)
394 return;
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_,
400 true));
401 var page = this.getMostVisiblePage();
402 this.window_.scrollTo(0, oldY * this.zoom_);
403 this.updateViewport_();
404 }.bind(this));
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_)
415 return;
416 var page = this.getMostVisiblePage();
417 // Fit to the current page's height and the widest page's width.
418 var dimensions = {
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_();
425 }.bind(this));
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_();
441 }.bind(this));
445 * Zoom in to the next predefined zoom level.
447 zoomIn: function() {
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_();
457 }.bind(this));
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)
467 return;
468 if (page < 0)
469 page = 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_();
476 }.bind(this));
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_,
490 true));
491 if (this.zoom_ > 1)
492 this.setZoomInternal_(1);
493 this.window_.scrollTo(0, 0);
495 this.contentSizeChanged_();
496 this.resize_();
497 }.bind(this));
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_) {
508 return {
509 x: 0,
510 y: 0,
511 width: 0,
512 height: 0
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);
541 return {
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_