Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / pdf / viewport.js
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.
4
5 /**
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
10  */
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;
19 }
20
21 /**
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
30  */
31 function Viewport(window,
32                   sizer,
33                   viewportChangedCallback,
34                   beforeZoomCallback,
35                   afterZoomCallback,
36                   scrollbarWidth) {
37   this.window_ = window;
38   this.sizer_ = sizer;
39   this.viewportChangedCallback_ = viewportChangedCallback;
40   this.beforeZoomCallback_ = beforeZoomCallback;
41   this.afterZoomCallback_ = afterZoomCallback;
42   this.allowedToChangeZoom_ = false;
43   this.zoom_ = 1;
44   this.documentDimensions_ = null;
45   this.pageDimensions_ = [];
46   this.scrollbarWidth_ = scrollbarWidth;
47   this.fittingType_ = Viewport.FittingType.NONE;
48
49   window.addEventListener('scroll', this.updateViewport_.bind(this));
50   window.addEventListener('resize', this.resize_.bind(this));
51 }
52
53 /**
54  * Enumeration of page fitting types.
55  * @enum {string}
56  */
57 Viewport.FittingType = {
58   NONE: 'none',
59   FIT_TO_PAGE: 'fit-to-page',
60   FIT_TO_WIDTH: 'fit-to-width'
61 };
62
63 /**
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
67  * these events.
68  */
69 Viewport.SCROLL_INCREMENT = 40;
70
71 /**
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.
75  */
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];
78
79 /**
80  * The minimum and maximum range to be used to clip zoom factor.
81  */
82 Viewport.ZOOM_FACTOR_RANGE = {
83   min: Viewport.ZOOM_FACTORS[0],
84   max: Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1]
85 };
86
87 /**
88  * The width of the page shadow around pages in pixels.
89  */
90 Viewport.PAGE_SHADOW = {top: 3, bottom: 7, left: 5, right: 5};
91
92 Viewport.prototype = {
93   /**
94    * @private
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
99    *     respectively.
100    */
101   documentNeedsScrollbars_: function(zoom) {
102     if (!this.documentDimensions_) {
103       return {
104         horizontal: false,
105         vertical: false
106       };
107     }
108     var documentWidth = this.documentDimensions_.width * zoom;
109     var documentHeight = this.documentDimensions_.height * zoom;
110     return {
111       horizontal: documentWidth > this.window_.innerWidth,
112       vertical: documentHeight > this.window_.innerHeight
113     };
114   },
115
116   /**
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
120    *     respectively.
121    */
122   documentHasScrollbars: function() {
123     return this.documentNeedsScrollbars_(this.zoom_);
124   },
125
126   /**
127    * @private
128    * Helper function called when the zoomed document size changes.
129    */
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';
136     }
137   },
138
139   /**
140    * @private
141    * Called when the viewport should be updated.
142    */
143   updateViewport_: function() {
144     this.viewportChangedCallback_();
145   },
146
147   /**
148    * @private
149    * Called when the viewport size changes.
150    */
151   resize_: function() {
152     if (this.fittingType_ == Viewport.FittingType.FIT_TO_PAGE)
153       this.fitToPage();
154     else if (this.fittingType_ == Viewport.FittingType.FIT_TO_WIDTH)
155       this.fitToWidth();
156     else
157       this.updateViewport_();
158   },
159
160   /**
161    * @type {Object} the scroll position of the viewport.
162    */
163   get position() {
164     return {
165       x: this.window_.pageXOffset,
166       y: this.window_.pageYOffset
167     };
168   },
169
170   /**
171    * Scroll the viewport to the specified position.
172    * @type {Object} position the position to scroll to.
173    */
174   set position(position) {
175     this.window_.scrollTo(position.x, position.y);
176   },
177
178   /**
179    * @type {Object} the size of the viewport excluding scrollbars.
180    */
181   get size() {
182     var needsScrollbars = this.documentNeedsScrollbars_(this.zoom_);
183     var scrollbarWidth = needsScrollbars.vertical ? this.scrollbarWidth_ : 0;
184     var scrollbarHeight = needsScrollbars.horizontal ? this.scrollbarWidth_ : 0;
185     return {
186       width: this.window_.innerWidth - scrollbarWidth,
187       height: this.window_.innerHeight - scrollbarHeight
188     };
189   },
190
191   /**
192    * @type {number} the zoom level of the viewport.
193    */
194   get zoom() {
195     return this.zoom_;
196   },
197
198   /**
199    * @private
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.
204    */
205   mightZoom_: function(f) {
206     this.beforeZoomCallback_();
207     this.allowedToChangeZoom_ = true;
208     f();
209     this.allowedToChangeZoom_ = false;
210     this.afterZoomCallback_();
211   },
212
213   /**
214    * @private
215    * Sets the zoom of the viewport.
216    * @param {number} newZoom the zoom level to zoom to.
217    */
218   setZoomInternal_: function(newZoom) {
219     if (!this.allowedToChangeZoom_) {
220       throw 'Called Viewport.setZoomInternal_ without calling ' +
221             'Viewport.mightZoom_.';
222     }
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_
227     ];
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);
233   },
234
235   /**
236    * Sets the zoom to the given zoom level.
237    * @param {number} newZoom the zoom level to zoom to.
238    */
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_();
245     }.bind(this));
246   },
247
248   /**
249    * @type {number} the width of scrollbars in the viewport in pixels.
250    */
251   get scrollbarWidth() {
252     return this.scrollbarWidth_;
253   },
254
255   /**
256    * @type {Viewport.FittingType} the fitting type the viewport is currently in.
257    */
258   get fittingType() {
259     return this.fittingType_;
260   },
261
262   /**
263    * @private
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.
266    */
267   getPageAtY_: function(y) {
268     var min = 0;
269     var max = this.pageDimensions_.length - 1;
270     while (max >= min) {
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.
274       var top = 0;
275       if (page > 0) {
276         top = this.pageDimensions_[page - 1].y +
277             this.pageDimensions_[page - 1].height;
278       }
279       var bottom = this.pageDimensions_[page].y +
280           this.pageDimensions_[page].height;
281
282       if (top <= y && bottom > y)
283         return page;
284       else if (top > y)
285         max = page - 1;
286       else
287         min = page + 1;
288     }
289     return 0;
290   },
291
292   /**
293    * Returns the page with the most pixels in the current viewport.
294    * @return {int} the index of the most visible page.
295    */
296   getMostVisiblePage: function() {
297     var firstVisiblePage = this.getPageAtY_(this.position.y / this.zoom_);
298     var mostVisiblePage = {number: 0, area: 0};
299     var viewportRect = {
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_
304     };
305     for (var i = firstVisiblePage; i < this.pageDimensions_.length; i++) {
306       var area = getIntersectionArea(this.pageDimensions_[i],
307                                      viewportRect);
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.
310       if (area == 0)
311         break;
312       if (area > mostVisiblePage.area) {
313         mostVisiblePage.area = area;
314         mostVisiblePage.number = i;
315       }
316     }
317     return mostVisiblePage.number;
318   },
319
320   /**
321    * @private
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
329    */
330   computeFittingZoom_: function(pageDimensions, widthOnly) {
331     // First compute the zoom without scrollbars.
332     var zoomWidth = this.window_.innerWidth / pageDimensions.width;
333     var zoom;
334     if (widthOnly) {
335       zoom = zoomWidth;
336     } else {
337       var zoomHeight = this.window_.innerHeight / pageDimensions.height;
338       zoom = Math.min(zoomWidth, zoomHeight);
339     }
340     // Check if there needs to be any scrollbars.
341     var needsScrollbars = this.documentNeedsScrollbars_(zoom);
342
343     // If the document fits, just return the zoom.
344     if (!needsScrollbars.horizontal && !needsScrollbars.vertical)
345       return zoom;
346
347     var zoomedDimensions = {
348       width: this.documentDimensions_.width * zoom,
349       height: this.documentDimensions_.height * zoom
350     };
351
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;
357     }
358     if (needsScrollbars.vertical &&
359         zoomedDimensions.width > this.window_.innerWidth - scrollbarWidth) {
360       needsScrollbars.horizontal = true;
361     }
362
363     // Compute available window space.
364     var windowWithScrollbars = {
365       width: this.window_.innerWidth,
366       height: this.window_.innerHeight
367     };
368     if (needsScrollbars.horizontal)
369       windowWithScrollbars.height -= scrollbarWidth;
370     if (needsScrollbars.vertical)
371       windowWithScrollbars.width -= scrollbarWidth;
372
373     // Recompute the zoom.
374     zoomWidth = windowWithScrollbars.width / pageDimensions.width;
375     if (widthOnly) {
376       zoom = zoomWidth;
377     } else {
378       var zoomHeight = windowWithScrollbars.height / pageDimensions.height;
379       zoom = Math.min(zoomWidth, zoomHeight);
380     }
381     return zoom;
382   },
383
384   /**
385    * Zoom the viewport so that the page-width consumes the entire viewport.
386    */
387   fitToWidth: function() {
388     this.mightZoom_(function() {
389       this.fittingType_ = Viewport.FittingType.FIT_TO_WIDTH;
390       if (!this.documentDimensions_)
391         return;
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_,
397                                                      true));
398       var page = this.getMostVisiblePage();
399       this.window_.scrollTo(0, oldY * this.zoom_);
400       this.updateViewport_();
401     }.bind(this));
402   },
403
404   /**
405    * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
406    * to the top of the most visible page.
407    */
408   fitToPage: function() {
409     this.mightZoom_(function() {
410       this.fittingType_ = Viewport.FittingType.FIT_TO_PAGE;
411       if (!this.documentDimensions_)
412         return;
413       var page = this.getMostVisiblePage();
414       // Fit to the current page's height and the widest page's width.
415       var dimensions = {
416         width: this.documentDimensions_.width,
417         height: this.pageDimensions_[page].height,
418       };
419       this.setZoomInternal_(this.computeFittingZoom_(dimensions, false));
420       this.window_.scrollTo(0, this.pageDimensions_[page].y * this.zoom_);
421       this.updateViewport_();
422     }.bind(this));
423   },
424
425   /**
426    * Zoom out to the next predefined zoom level.
427    */
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];
435       }
436       this.setZoomInternal_(nextZoom);
437       this.updateViewport_();
438     }.bind(this));
439   },
440
441   /**
442    * Zoom in to the next predefined zoom level.
443    */
444   zoomIn: function() {
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];
451       }
452       this.setZoomInternal_(nextZoom);
453       this.updateViewport_();
454     }.bind(this));
455   },
456
457   /**
458    * Go to the given page index.
459    * @param {number} page the index of the page to go to. zero-based.
460    */
461   goToPage: function(page) {
462     this.mightZoom_(function() {
463       if (this.pageDimensions_.length == 0)
464         return;
465       if (page < 0)
466         page = 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_();
473     }.bind(this));
474   },
475
476   /**
477    * Set the dimensions of the document.
478    * @param {Object} documentDimensions the dimensions of the document
479    */
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_,
487                                                        true));
488         if (this.zoom_ > 1)
489           this.setZoomInternal_(1);
490         this.window_.scrollTo(0, 0);
491       }
492       this.contentSizeChanged_();
493       this.resize_();
494     }.bind(this));
495   },
496
497   /**
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.
502    */
503   getPageScreenRect: function(page) {
504     if (!this.documentDimensions_) {
505       return {
506         x: 0,
507         y: 0,
508         width: 0,
509         height: 0
510       };
511     }
512     if (page >= this.pageDimensions_.length)
513       page = this.pageDimensions_.length - 1;
514
515     var pageDimensions = this.pageDimensions_[page];
516
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
525     };
526
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);
537
538     return {
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_
543     };
544   }
545 };