diff options
| author | Stephen Blott | 2014-11-09 14:33:46 +0000 | 
|---|---|---|
| committer | Stephen Blott | 2014-11-09 16:37:31 +0000 | 
| commit | b8b1644a306c1fe12b5faa5204630eb30f1e64b3 (patch) | |
| tree | 3029c1f2f516acb9d1fc1c8b7dfd03ba06e2100b | |
| parent | 1010b7868eb59dde70bc6faf8fd6fb2969688e48 (diff) | |
| download | vimium-b8b1644a306c1fe12b5faa5204630eb30f1e64b3.tar.bz2 | |
Smooth scroll; handle chrome bug and refactor.
| -rw-r--r-- | content_scripts/scroller.coffee | 125 | 
1 files changed, 62 insertions, 63 deletions
| diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 62a930fc..34f9b148 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -36,40 +36,51 @@ getDimension = (el, direction, amount) ->      amount  # Test whether element should be scrolled. -isScrollable = (element, direction) -> +isScrollAllowed = (element, direction) ->    # Elements with `overflow: hidden` should not be scrolled. -  overflow = window.getComputedStyle(element).getPropertyValue("overflow-#{direction}") -  return false if overflow == "hidden" -  return true - -# Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149). -# Therefore we cannot figure out if we have scrolled to the bottom of an element by testing if scrollTop + -# clientHeight == scrollHeight. So just try to increase scrollTop blindly -- if it fails we know we have -# reached the end of the content. -ensureScrollChange = (direction, changeFn) -> +  window.getComputedStyle(element).getPropertyValue("overflow-#{direction}") != "hidden" + +# Test whether element actually scrolls in the direction required when asked to do so. +# Due to chrome bug 110149, scrollHeight and clientHeight cannot be used to reliably determine whether an +# element will scroll.  Instead, we scroll the element by 1 or -1 and see if it moved. +isScrollPossible = (element, direction, amount, factor) -> +  axisName = scrollProperties[direction].axisName +  # delta, here, is treated as a relative amount, which is correct for relative scrolls. For absolute scrolls +  # (only gg, G, and friends), amount can be either 'max' or zero. In the former case, we're definitely +  # scrolling forwards, so any positive value will do for delta.  In the latter case, we're definitely +  # scrolling backwards, so a delta of -1 will do. +  delta = factor * getDimension(element, direction, amount) || -1 +  delta = delta / Math.abs delta # 1 or -1 +  before = element[axisName] +  element[axisName] += delta +  after = element[axisName] +  element[axisName] = before +  before != after + +# Find the element we should and can scroll. +findScrollableElement = (element, direction, amount, factor = 1) ->    axisName = scrollProperties[direction].axisName -  element = activatedElement -  progress = 0 -  loop -    oldScrollValue = element[axisName] -    changeFn(element, axisName) if isScrollable element, direction -    progress += element[axisName] - oldScrollValue -    break unless element[axisName] == oldScrollValue && element != document.body -    # we may have an orphaned element. if so, just scroll the body element. -    element = element.parentElement || document.body - -  # if the activated element has been scrolled completely offscreen, subsequent changes in its scroll -  # position will not provide any more visual feedback to the user. therefore we deactivate it so that -  # subsequent scrolls only move the parent element. -  rect = activatedElement.getBoundingClientRect() -  if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) -    activatedElement = element +  while element != document.body and +    not (isScrollPossible(element, direction, amount, factor) and isScrollAllowed(element, direction)) +      element = element.parentElement || document.body +  element + +performScroll = (element, axisName, amount, checkVisibility = true) -> +  before = element[axisName] +  element[axisName] += amount + +  if checkVisibility +    # if the activated element has been scrolled completely offscreen, subsequent changes in its scroll +    # position will not provide any more visual feedback to the user. therefore we deactivate it so that +    # subsequent scrolls only move the parent element. +    rect = activatedElement.getBoundingClientRect() +    if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) +      activatedElement = element +    # Return the amount by which the scroll position has changed. -  return progress +  element[axisName] - before -# Scroll by a relative amount in some direction, possibly smoothly. -# The constants below seem to roughly match chrome's scroll speeds for both short and long scrolls. -# TODO(smblott) For very-long scrolls, chrome implements a soft landing; we don't. +# Scroll by a relative amount (a number) in some direction, possibly smoothly.  doScrollBy = do ->    interval = 10 # Update interval (in ms).    duration = 120 # This must be a multiple of interval (also in ms). @@ -87,30 +98,27 @@ doScrollBy = do ->      # Ensure we have a multiple of interval.      return interval * Math.round (extra / interval) -  scroller = (direction,amount) -> -    return ensureScrollChange direction, (element, axisName) -> element[axisName] += amount - -  (direction,amount,wantSmooth) -> +  (element, direction, amount, wantSmooth) -> +    axisName = scrollProperties[direction].axisName      clearTimer()      unless wantSmooth and settings.get "smoothScroll" -      scroller direction, amount -      return +      return performScroll element, axisName, amount      requiredTicks = (duration + calculateExtraDuration amount) / interval -    # Round away from 0, so that we don't leave any requested scroll amount unscrolled. -    rounder = (if 0 <= amount then Math.ceil else Math.floor) -    delta = rounder(amount / requiredTicks) +    # Round away from 0, so that we don't leave any scroll amount unscrolled. +    delta = (if 0 <= amount then Math.ceil else Math.floor)(amount / requiredTicks) -    ticks = 0 -    ticker = -> -      # If we haven't scrolled by the expected amount, then we've hit the top, bottom or side of the activated -      # element, so stop scrolling. -      if scroller(direction, delta) != delta or ++ticks == requiredTicks -        clearTimer() +    if delta +      ticks = 0 +      ticker = -> +        if performScroll(element, axisName, delta, false) != delta or ++ticks == requiredTicks +          # One final call of performScroll to check the visibility of the activated element. +          performScroll(element, axisName, 0, true) +          clearTimer() -    timer = setInterval ticker, interval -    ticker() +      timer = setInterval ticker, interval +      ticker()  # scroll the active element in :direction by :amount * :factor.  # :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions. @@ -126,28 +134,19 @@ root.scrollBy = (direction, amount, factor = 1) ->    if (!activatedElement || !isRendered(activatedElement))      activatedElement = document.body -  elementAmount = getDimension activatedElement, direction, amount -  elementAmount *= factor - -  doScrollBy direction, elementAmount, true +  element = findScrollableElement activatedElement, direction, amount, factor +  elementAmount = factor * getDimension element, direction, amount +  doScrollBy element, direction, elementAmount, true -root.scrollTo = (direction, pos, wantSmooth=false) -> +root.scrollTo = (direction, pos, wantSmooth = false) ->    return unless document.body    if (!activatedElement || !isRendered(activatedElement))      activatedElement = document.body -  # Find the deepest scrollable element which would move if we scrolled it.  This is the element which -  # ensureScrollChange will scroll. -  # TODO(smblott) We're pretty much copying what ensureScrollChange does here.  Refactor. -  element = activatedElement -  axisName = scrollProperties[direction].axisName -  while element != document.body and -    (getDimension(element, direction, pos) == element[axisName] or not isScrollable element, direction) -      element = element.parentElement || document.body - -  amount = getDimension(element,direction,pos) - element[axisName] -  doScrollBy direction, amount, wantSmooth +  element = findScrollableElement activatedElement, direction, pos +  amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName] +  doScrollBy element, direction, amount, wantSmooth  # TODO refactor and put this together with the code in getVisibleClientRect  isRendered = (element) -> | 
