From 1f678d685eb39e5269cfe8f87ac99522aa1b5200 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 7 Nov 2014 10:58:39 +0000 Subject: Smooth scrolling. --- content_scripts/scroller.coffee | 83 ++++++++++++++++++++++++++++------ content_scripts/vimium_frontend.coffee | 12 ++--- 2 files changed, 74 insertions(+), 21 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index b3a14c78..4d1109c9 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -5,8 +5,10 @@ window.Scroller = root = {} # input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document. # activatedElement = null +settings = null -root.init = -> +root.init = (frontendSettings) -> + settings = frontendSettings handlerStack.push DOMActivate: -> activatedElement = event.target scrollProperties = @@ -36,11 +38,13 @@ getDimension = (el, direction, name) -> ensureScrollChange = (direction, changeFn) -> axisName = scrollProperties[direction].axisName element = activatedElement + progress = 0 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" + 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 @@ -51,6 +55,53 @@ ensureScrollChange = (direction, changeFn) -> 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 + +# 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. +doScrollBy = do -> + interval = 10 # Update interval (in ms). + duration = 120 # This must be a multiple of interval (also in ms). + fudgeFactor = 25 + timer = null + + clearTimer = -> + if timer + clearInterval timer + timer = null + + # Allow a bit longer for longer scrolls. + calculateExtraDuration = (amount) -> + extra = fudgeFactor * Math.log Math.abs amount + # 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) -> + clearTimer() + + unless wantSmooth and settings.get "smoothScroll" + scroller direction, amount + return + + 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) + + 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() + + 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. @@ -66,26 +117,28 @@ root.scrollBy = (direction, amount, factor = 1) -> if (!activatedElement || !isRendered(activatedElement)) activatedElement = document.body - ensureScrollChange direction, (element, axisName) -> - if Utils.isString amount - elementAmount = getDimension element, direction, amount - else - elementAmount = amount - elementAmount *= factor - element[axisName] += elementAmount + if Utils.isString amount + elementAmount = getDimension activatedElement, direction, amount + else + elementAmount = amount + elementAmount *= factor + + doScrollBy direction, elementAmount, true -root.scrollTo = (direction, pos) -> +root.scrollTo = (direction, pos, wantSmooth=false) -> return unless document.body if (!activatedElement || !isRendered(activatedElement)) activatedElement = document.body - ensureScrollChange direction, (element, axisName) -> - if Utils.isString pos - elementPos = getDimension element, direction, pos - else - elementPos = pos - element[axisName] = elementPos + if Utils.isString pos + elementPos = getDimension activatedElement, direction, pos + else + elementPos = pos + axisName = scrollProperties[direction].axisName + elementAmount = elementPos - activatedElement[axisName] + + doScrollBy direction, elementAmount, wantSmooth # TODO refactor and put this together with the code in getVisibleClientRect isRendered = (element) -> 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 -- cgit v1.2.3 From 1010b7868eb59dde70bc6faf8fd6fb2969688e48 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 7 Nov 2014 14:49:54 +0000 Subject: smooth scroll; fix absolute scrolling of active element. --- content_scripts/scroller.coffee | 47 ++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 19 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 4d1109c9..62a930fc 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -23,13 +23,24 @@ 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 + +# Test whether element should be scrolled. +isScrollable = (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 + @@ -41,9 +52,7 @@ ensureScrollChange = (direction, changeFn) -> progress = 0 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" + 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. @@ -117,10 +126,7 @@ root.scrollBy = (direction, amount, factor = 1) -> if (!activatedElement || !isRendered(activatedElement)) activatedElement = document.body - if Utils.isString amount - elementAmount = getDimension activatedElement, direction, amount - else - elementAmount = amount + elementAmount = getDimension activatedElement, direction, amount elementAmount *= factor doScrollBy direction, elementAmount, true @@ -131,14 +137,17 @@ root.scrollTo = (direction, pos, wantSmooth=false) -> if (!activatedElement || !isRendered(activatedElement)) activatedElement = document.body - if Utils.isString pos - elementPos = getDimension activatedElement, direction, pos - else - elementPos = pos + # 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 - elementAmount = elementPos - activatedElement[axisName] + while element != document.body and + (getDimension(element, direction, pos) == element[axisName] or not isScrollable element, direction) + element = element.parentElement || document.body - doScrollBy direction, elementAmount, wantSmooth + amount = getDimension(element,direction,pos) - element[axisName] + doScrollBy direction, amount, wantSmooth # TODO refactor and put this together with the code in getVisibleClientRect isRendered = (element) -> -- cgit v1.2.3 From b8b1644a306c1fe12b5faa5204630eb30f1e64b3 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 9 Nov 2014 14:33:46 +0000 Subject: Smooth scroll; handle chrome bug and refactor. --- content_scripts/scroller.coffee | 125 ++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 63 deletions(-) (limited to 'content_scripts') 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) -> -- cgit v1.2.3 From df521c26fda9b8d3e8c182fc85deaf5b8c723cd4 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 10 Nov 2014 08:27:55 +0000 Subject: Change smooth scrolling to requestAnimationFrame, tidy up scroller code --- content_scripts/scroller.coffee | 128 ++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 70 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 34f9b148..2e0d08ad 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -1,5 +1,3 @@ -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. @@ -7,10 +5,6 @@ window.Scroller = root = {} activatedElement = null settings = null -root.init = (frontendSettings) -> - settings = frontendSettings - handlerStack.push DOMActivate: -> activatedElement = event.target - scrollProperties = x: { axisName: 'scrollLeft' @@ -37,8 +31,11 @@ getDimension = (el, direction, amount) -> # Test whether element should be scrolled. isScrollAllowed = (element, direction) -> + computedStyle = window.getComputedStyle(element) # Elements with `overflow: hidden` should not be scrolled. - window.getComputedStyle(element).getPropertyValue("overflow-#{direction}") != "hidden" + 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 @@ -58,7 +55,7 @@ isScrollPossible = (element, direction, amount, factor) -> before != after # Find the element we should and can scroll. -findScrollableElement = (element, direction, amount, factor = 1) -> +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)) @@ -81,75 +78,66 @@ performScroll = (element, axisName, amount, checkVisibility = true) -> element[axisName] - before # 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). - fudgeFactor = 25 - timer = null +doScrollBy = (element, direction, amount, wantSmooth) -> + axisName = scrollProperties[direction].axisName - clearTimer = -> - if timer - clearInterval timer - timer = null + unless wantSmooth and settings.get "smoothScroll" + return performScroll element, axisName, amount + + duration = 100 # Duration in ms. + fudgeFactor = 25 # Allow a bit longer for longer scrolls. - calculateExtraDuration = (amount) -> - extra = fudgeFactor * Math.log Math.abs amount - # Ensure we have a multiple of interval. - return interval * Math.round (extra / interval) - - (element, direction, amount, wantSmooth) -> - axisName = scrollProperties[direction].axisName - clearTimer() - - unless wantSmooth and settings.get "smoothScroll" - return performScroll element, axisName, amount - - requiredTicks = (duration + calculateExtraDuration amount) / interval - # 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) - - 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() - -# 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) + duration += fudgeFactor * Math.log Math.abs amount + + 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 - window.scrollBy(0, amount) - return + animatorId = window.requestAnimationFrame(animate) + + animatorId = window.requestAnimationFrame(animate) - if (!activatedElement || !isRendered(activatedElement)) - activatedElement = document.body +Scroller = + init: (frontendSettings) -> + settings = frontendSettings + handlerStack.push DOMActivate: -> activatedElement = event.target - element = findScrollableElement activatedElement, direction, amount, factor - elementAmount = factor * getDimension element, direction, amount - doScrollBy element, direction, elementAmount, true + # 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 -root.scrollTo = (direction, pos, wantSmooth = false) -> - return unless document.body + element = findScrollableElement activatedElement, direction, amount, factor + elementAmount = factor * getDimension element, direction, amount + doScrollBy element, direction, elementAmount, true - if (!activatedElement || !isRendered(activatedElement)) - activatedElement = document.body + 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 + 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) -> - computedStyle = window.getComputedStyle(element, null) - return !(computedStyle.getPropertyValue("visibility") != "visible" || - computedStyle.getPropertyValue("display") == "none") +root = exports ? window +root.Scroller = Scroller -- cgit v1.2.3