diff options
| -rw-r--r-- | background_scripts/settings.coffee | 1 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 183 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 12 | ||||
| -rw-r--r-- | pages/options.coffee | 1 | ||||
| -rw-r--r-- | pages/options.html | 9 | 
5 files changed, 133 insertions, 73 deletions
| diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index d6e8fcde..f68a51d7 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -61,6 +61,7 @@ root.Settings = Settings =    # or strings    defaults:      scrollStepSize: 60 +    smoothScroll: false      keyMappings: "# Insert your prefered key mappings here."      linkHintCharacters: "sadfjklewcmpgh"      linkHintNumbers: "0123456789" diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index b3a14c78..2e0d08ad 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -1,13 +1,9 @@ -window.Scroller = root = {} -  #  # activatedElement is different from document.activeElement -- the latter seems to be reserved mostly for  # input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document.  #  activatedElement = null - -root.init = -> -  handlerStack.push DOMActivate: -> activatedElement = event.target +settings = null  scrollProperties =    x: { @@ -21,74 +17,127 @@ scrollProperties =      viewSize: 'clientWidth'    } -getDimension = (el, direction, name) -> -  # the clientSizes of the body are the dimensions of the entire page, but the viewport should only be the -  # part visible through the window -  if name is 'viewSize' and el is document.body -    if direction is 'x' then window.innerWidth else window.innerHeight +getDimension = (el, direction, amount) -> +  if Utils.isString amount +    name = amount +    # the clientSizes of the body are the dimensions of the entire page, but the viewport should only be the +    # part visible through the window +    if name is 'viewSize' and el is document.body +      if direction is 'x' then window.innerWidth else window.innerHeight +    else +      el[scrollProperties[direction][name]]    else -    el[scrollProperties[direction][name]] +    amount -# 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) -> +# Test whether element should be scrolled. +isScrollAllowed = (element, direction) -> +  computedStyle = window.getComputedStyle(element) +  # Elements with `overflow: hidden` should not be scrolled. +  return computedStyle.getPropertyValue("overflow-#{direction}") != "hidden" and +         ["hidden", "collapse"].indexOf(computedStyle.getPropertyValue("visibility")) == -1 and +         computedStyle.getPropertyValue("display") != "none" + +# 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 -  element = activatedElement -  loop -    oldScrollValue = element[axisName] -    # Elements with `overflow: hidden` should not be scrolled. -    overflow = window.getComputedStyle(element).getPropertyValue("overflow-#{direction}") -    changeFn(element, axisName) unless overflow == "hidden" -    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 - -# 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. -root.scrollBy = (direction, amount, factor = 1) -> -  # if this is called before domReady, just use the window scroll function -  if (!document.body and amount instanceof Number) -    if (direction == "x") -      window.scrollBy(amount, 0) -    else -      window.scrollBy(0, amount) -    return +  # 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 = document.body, direction, amount, factor = 1) -> +  axisName = scrollProperties[direction].axisName +  while element != document.body and +    not (isScrollPossible(element, direction, amount, factor) and isScrollAllowed(element, direction)) +      element = element.parentElement || document.body +  element -  if (!activatedElement || !isRendered(activatedElement)) -    activatedElement = document.body +performScroll = (element, axisName, amount, checkVisibility = true) -> +  before = element[axisName] +  element[axisName] += amount -  ensureScrollChange direction, (element, axisName) -> -    if Utils.isString amount -      elementAmount = getDimension element, direction, amount -    else -      elementAmount = amount -    elementAmount *= factor -    element[axisName] += elementAmount +  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. +  element[axisName] - before + +# Scroll by a relative amount (a number) in some direction, possibly smoothly. +doScrollBy = (element, direction, amount, wantSmooth) -> +  axisName = scrollProperties[direction].axisName + +  unless wantSmooth and settings.get "smoothScroll" +    return performScroll element, axisName, amount -root.scrollTo = (direction, pos) -> -  return unless document.body +  duration = 100 # Duration in ms. +  fudgeFactor = 25 -  if (!activatedElement || !isRendered(activatedElement)) -    activatedElement = document.body +  # Allow a bit longer for longer scrolls. +  duration += fudgeFactor * Math.log Math.abs amount -  ensureScrollChange direction, (element, axisName) -> -    if Utils.isString pos -      elementPos = getDimension element, direction, pos +  roundOut = if 0 <= amount then Math.ceil else Math.floor + +  # Round away from 0, so that we don't leave any scroll amount unscrolled. +  delta = roundOut(amount / duration) + +  animatorId = null +  start = null +  lastTime = null +  scrolledAmount = 0 + +  animate = (timestamp) -> +    start ?= timestamp + +    progress = Math.min(timestamp - start, duration) +    scrollDelta = roundOut(delta * progress) - scrolledAmount +    scrolledAmount += scrollDelta + +    if performScroll(element, axisName, scrollDelta, false) != scrollDelta or +       progress >= duration +      # One final call of performScroll to check the visibility of the activated element. +      performScroll(element, axisName, 0, true) +      window.cancelAnimationFrame(animatorId)      else -      elementPos = pos -    element[axisName] = elementPos - -# TODO refactor and put this together with the code in getVisibleClientRect -isRendered = (element) -> -  computedStyle = window.getComputedStyle(element, null) -  return !(computedStyle.getPropertyValue("visibility") != "visible" || -      computedStyle.getPropertyValue("display") == "none") +      animatorId = window.requestAnimationFrame(animate) + +  animatorId = window.requestAnimationFrame(animate) + +Scroller = +  init: (frontendSettings) -> +    settings = frontendSettings +    handlerStack.push DOMActivate: -> activatedElement = event.target + +  # 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. +  scrollBy: (direction, amount, factor = 1) -> +    # if this is called before domReady, just use the window scroll function +    return unless document.body + +    element = findScrollableElement activatedElement, direction, amount, factor +    elementAmount = factor * getDimension element, direction, amount +    doScrollBy element, direction, elementAmount, true + +  scrollTo: (direction, pos, wantSmooth = false) -> +    return unless document.body + +    element = findScrollableElement activatedElement, direction, pos +    amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName] +    doScrollBy element, direction, amount, wantSmooth + +root = exports ? window +root.Scroller = Scroller diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 118f985e..57503565 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -49,7 +49,7 @@ settings =    loadedValues: 0    valuesToLoad: ["scrollStepSize", "linkHintCharacters", "linkHintNumbers", "filterLinkHints", "hideHud",      "previousPatterns", "nextPatterns", "findModeRawQuery", "regexFindMode", "userDefinedLinkHintCss", -    "helpDialog_showAdvancedCommands"] +    "helpDialog_showAdvancedCommands", "smoothScroll"]    isLoaded: false    eventListeners: {} @@ -101,7 +101,7 @@ initializePreDomReady = ->    settings.addEventListener("load", LinkHints.init.bind(LinkHints))    settings.load() -  Scroller.init() +  Scroller.init settings    checkIfEnabledForUrl() @@ -227,10 +227,10 @@ window.focusThisFrame = (shouldHighlight) ->      setTimeout((-> document.body.style.border = borderWas), 200)  extend window, -  scrollToBottom: -> Scroller.scrollTo "y", "max" -  scrollToTop: -> Scroller.scrollTo "y", 0 -  scrollToLeft: -> Scroller.scrollTo "x", 0 -  scrollToRight: -> Scroller.scrollTo "x", "max" +  scrollToBottom: -> Scroller.scrollTo "y", "max", true +  scrollToTop: -> Scroller.scrollTo "y", 0, true +  scrollToLeft: -> Scroller.scrollTo "x", 0, true +  scrollToRight: -> Scroller.scrollTo "x", "max", true    scrollUp: -> Scroller.scrollBy "y", -1 * settings.get("scrollStepSize")    scrollDown: -> Scroller.scrollBy "y", settings.get("scrollStepSize")    scrollPageUp: -> Scroller.scrollBy "y", "viewSize", -1/2 diff --git a/pages/options.coffee b/pages/options.coffee index f5968eb9..3474bcba 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -196,6 +196,7 @@ document.addEventListener "DOMContentLoaded", ->      previousPatterns: NonEmptyTextOption      regexFindMode: CheckBoxOption      scrollStepSize: NumberOption +    smoothScroll: CheckBoxOption      searchEngines: TextOption      searchUrl: NonEmptyTextOption      userDefinedLinkHintCss: TextOption diff --git a/pages/options.html b/pages/options.html index 4f037ba5..d6ce2764 100644 --- a/pages/options.html +++ b/pages/options.html @@ -360,6 +360,15 @@ unmapAll              </td>            </tr>            <tr> +            <td class="caption"></td> +            <td verticalAlign="top" class="booleanOption"> +              <label> +                <input id="smoothScroll" type="checkbox"/> +                Use smooth scrolling. +              </label> +            </td> +          </tr> +          <tr>              <td class="caption">Previous patterns</td>              <td verticalAlign="top">                  <div class="help"> | 
