Home | History | Annotate | Download | only in actions
      1 // Copyright (c) 2012 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 // This file provides the ScrollAction object, which scrolls a page
      6 // from top to bottom:
      7 //   1. var action = new __ScrollAction(callback)
      8 //   2. action.start(scroll_options)
      9 'use strict';
     10 
     11 (function() {
     12   var MAX_SCROLL_LENGTH_PIXELS = 5000;
     13 
     14   var getTimeMs = (function() {
     15     if (window.performance)
     16       return (performance.now       ||
     17               performance.mozNow    ||
     18               performance.msNow     ||
     19               performance.oNow      ||
     20               performance.webkitNow).bind(window.performance);
     21     else
     22       return function() { return new Date().getTime(); };
     23   })();
     24 
     25   var requestAnimationFrame = (function() {
     26     return window.requestAnimationFrame       ||
     27            window.webkitRequestAnimationFrame ||
     28            window.mozRequestAnimationFrame    ||
     29            window.oRequestAnimationFrame      ||
     30            window.msRequestAnimationFrame     ||
     31            function(callback) {
     32              window.setTimeout(callback, 1000 / 60);
     33            };
     34   })().bind(window);
     35 
     36   function ScrollGestureOptions(opt_options) {
     37     if (opt_options) {
     38       this.element_ = opt_options.element;
     39       this.left_start_percentage_ = opt_options.left_start_percentage;
     40       this.top_start_percentage_ = opt_options.top_start_percentage;
     41     } else {
     42       this.element_ = document.body;
     43       this.left_start_percentage_ = 0.5;
     44       this.top_start_percentage_ = 0.5;
     45     }
     46   }
     47 
     48   /**
     49    * Scrolls a given element down a certain amount to emulate user scroll.
     50    * Uses smooth scroll capabilities provided by the platform, if available.
     51    * @constructor
     52    */
     53   function SmoothScrollDownGesture(options) {
     54     this.options_ = options;
     55   };
     56 
     57   function min(a, b) {
     58     if (a > b) {
     59       return b;
     60     }
     61     return a;
     62   };
     63 
     64   function getBoundingVisibleRect(el) {
     65     var bound = el.getBoundingClientRect();
     66     var rect = { top: bound.top,
     67                  left: bound.left,
     68                  width: bound.width,
     69                  height: bound.height };
     70     var outsideHeight = (rect.top + rect.height) - window.innerHeight;
     71     var outsideWidth = (rect.left + rect.width) - window.innerWidth;
     72 
     73     if (outsideHeight > 0) {
     74       rect.height -= outsideHeight;
     75     }
     76     if (outsideWidth > 0) {
     77       rect.width -= outsideWidth;
     78     }
     79     return rect;
     80   };
     81 
     82   SmoothScrollDownGesture.prototype.start = function(distance, callback) {
     83     this.callback_ = callback;
     84     if (window.chrome &&
     85         chrome.gpuBenchmarking &&
     86         chrome.gpuBenchmarking.smoothScrollBy) {
     87       var rect = getBoundingVisibleRect(this.options_.element_);
     88       var start_left =
     89           rect.left + rect.width * this.options_.left_start_percentage_;
     90       var start_top =
     91           rect.top + rect.height * this.options_.top_start_percentage_;
     92       chrome.gpuBenchmarking.smoothScrollBy(distance, function() {
     93         callback();
     94       }, start_left, start_top);
     95       return;
     96     }
     97 
     98     var SCROLL_DELTA = 100;
     99     this.options_.element_.scrollTop += SCROLL_DELTA;
    100     requestAnimationFrame(callback);
    101   };
    102 
    103   // This class scrolls a page from the top to the bottom once.
    104   //
    105   // The page is scrolled down by a set of scroll gestures. These gestures
    106   // correspond to a reading gesture on that platform.
    107   //
    108   // start -> startPass_ -> ...scroll... -> onGestureComplete_ ->
    109   //       -> startPass_ -> .. scroll... -> onGestureComplete_ -> callback_
    110   function ScrollAction(opt_callback, opt_remaining_distance_func) {
    111     var self = this;
    112 
    113     this.beginMeasuringHook = function() {}
    114     this.endMeasuringHook = function() {}
    115 
    116     this.callback_ = opt_callback;
    117     this.remaining_distance_func_ = opt_remaining_distance_func;
    118   }
    119 
    120   ScrollAction.prototype.getRemainingScrollDistance_ = function() {
    121     if (this.remaining_distance_func_)
    122       return this.remaining_distance_func_();
    123 
    124     var clientHeight;
    125     // clientHeight is "special" for the body element.
    126     if (this.element_ == document.body)
    127       clientHeight = window.innerHeight;
    128     else
    129       clientHeight = this.element_.clientHeight;
    130 
    131     return this.scrollHeight_ - this.element_.scrollTop - clientHeight;
    132   }
    133 
    134   ScrollAction.prototype.start = function(opt_options) {
    135     this.options_ = new ScrollGestureOptions(opt_options);
    136     // Assign this.element_ here instead of constructor, because the constructor
    137     // ensures this method will be called after the document is loaded.
    138     this.element_ = this.options_.element_;
    139     // Some pages load more content when you scroll to the bottom. Record
    140     // the original element height here and only scroll to that point.
    141     // -1 to allow for rounding errors on scaled viewports (like mobile).
    142     this.scrollHeight_ = Math.min(MAX_SCROLL_LENGTH_PIXELS,
    143                                   this.element_.scrollHeight - 1);
    144     requestAnimationFrame(this.startPass_.bind(this));
    145   };
    146 
    147   ScrollAction.prototype.startPass_ = function() {
    148     this.element_.scrollTop = 0;
    149 
    150     this.beginMeasuringHook();
    151 
    152     this.gesture_ = new SmoothScrollDownGesture(this.options_);
    153     this.gesture_.start(this.getRemainingScrollDistance_(),
    154                         this.onGestureComplete_.bind(this));
    155   };
    156 
    157   ScrollAction.prototype.getResults = function() {
    158     return this.renderingStats_;
    159   }
    160 
    161   ScrollAction.prototype.onGestureComplete_ = function() {
    162     // If the scrollHeight went down, only scroll to the new scrollHeight.
    163     // -1 to allow for rounding errors on scaled viewports (like mobile).
    164     this.scrollHeight_ = Math.min(this.scrollHeight_,
    165                                   this.element_.scrollHeight - 1);
    166 
    167     if (this.getRemainingScrollDistance_() > 0) {
    168       this.gesture_.start(this.getRemainingScrollDistance_(),
    169                           this.onGestureComplete_.bind(this));
    170       return;
    171     }
    172 
    173     this.endMeasuringHook();
    174 
    175     // We're done.
    176     if (this.callback_)
    177       this.callback_();
    178   };
    179 
    180   window.__ScrollAction = ScrollAction;
    181   window.__ScrollAction_GetBoundingVisibleRect = getBoundingVisibleRect;
    182 })();
    183