tridactyl/src/scrolling.ts

205 lines
7.3 KiB
TypeScript
Raw Normal View History

2018-05-26 17:31:27 +02:00
import * as Native from "./native_background"
type scrollingDirection = "scrollLeft" | "scrollTop"
// Stores elements that are currently being horizontally scrolled
let horizontallyScrolling = new Map()
// Stores elements that are currently being vertically scrolled
let verticallyScrolling = new Map()
let opts = { smooth: null, duration: null }
2018-05-26 17:31:27 +02:00
async function getSmooth() {
if (opts.smooth === null)
opts.smooth = await Native.getConfElsePrefElseDefault(
"smoothscroll",
"general.smoothScroll",
"false",
)
return opts.smooth
2018-05-26 17:31:27 +02:00
}
async function getDuration() {
if (opts.duration === null)
opts.duration = await Native.getConfElsePrefElseDefault(
"scrollduration",
"general.smoothScroll.lines.durationMinMs",
100,
)
return opts.duration
2018-05-26 17:31:27 +02:00
}
browser.storage.onChanged.addListener(changes => {
if ("userconfig" in changes) {
if ("smoothscroll" in changes.userconfig.newValue)
opts.smooth = changes.userconfig.newValue["smoothscroll"]
if ("scrollduration" in changes.userconfig.newValue)
opts.duration = changes.userconfig.newValue["scrollduration"]
}
})
2018-05-26 17:31:27 +02:00
class ScrollingData {
// time at which the scrolling animation started
startTime: number
// Starting position of the element. This shouldn't ever change.
startPos: number
// Where the element should end up. This can change if .scroll() is called
// while a scrolling animation is already running
endPos: number
// Whether the element is being scrolled
scrolling = false
// Duration of the scrolling animation
duration = 0
/** elem: The element that should be scrolled
* pos: "scrollLeft" if the element should be scrolled on the horizontal axis, "scrollTop" otherwise
*/
constructor(
private elem: HTMLElement,
private pos: scrollingDirection = "scrollTop",
) {}
/** Computes where the element should be.
* This changes depending on how long ago the first scrolling attempt was
* made.
* It might be useful to make this function more configurable by making it
* accept an argument instead of using performance.now()
*/
getStep() {
if (this.startTime === undefined) {
this.startTime = performance.now()
}
let elapsed = performance.now() - this.startTime
// If the animation should be done, return the position the element should have
if (elapsed > this.duration || this.elem[this.pos] == this.endPos)
return this.endPos
let result = (this.endPos - this.startPos) * elapsed / this.duration
if (result >= 1 || result <= -1) return this.startPos + result
return this.elem[this.pos] + (this.startPos < this.endPos ? 1 : -1)
}
/** Updates the position of this.elem */
scrollStep() {
let val = this.elem[this.pos]
this.elem[this.pos] = this.getStep()
return val != this.elem[this.pos]
}
/** Calls this.scrollStep() until the element has been completely scrolled
* or the scrolling animation is complete */
scheduleStep() {
// If scrollStep() scrolled the element, reschedule a step
// Otherwise, register that the element stopped scrolling
window.requestAnimationFrame(
() =>
this.scrollStep()
? this.scheduleStep()
: (this.scrolling = false),
)
}
scroll(distance: number, duration: number) {
this.startTime = performance.now()
this.startPos = this.elem[this.pos]
this.endPos = this.elem[this.pos] + distance
this.duration = duration
// If we're already scrolling we don't need to try to scroll
if (this.scrolling) return true
this.scrolling = this.scrollStep()
if (this.scrolling)
// If the element can be scrolled, scroll until animation completion
this.scheduleStep()
return this.scrolling
}
}
/** Tries to scroll e by x and y pixel, make the smooth scrolling animation
* last duration milliseconds
*/
export async function scroll(
x: number,
y: number,
e: HTMLElement,
duration: number = undefined,
) {
let smooth = await getSmooth()
if (smooth == "false") duration = 0
else if (duration === undefined) duration = await getDuration()
let result = false
if (x != 0) {
// Don't create a new ScrollingData object if the element is already
// being scrolled
let scrollData = horizontallyScrolling.get(e)
if (!scrollData) scrollData = new ScrollingData(e, "scrollLeft")
2018-05-26 17:31:27 +02:00
horizontallyScrolling.set(e, scrollData)
result = result || scrollData.scroll(x, duration)
}
if (y != 0) {
let scrollData = verticallyScrolling.get(e)
if (!scrollData) scrollData = new ScrollingData(e, "scrollTop")
2018-05-26 17:31:27 +02:00
verticallyScrolling.set(e, scrollData)
result = result || scrollData.scroll(y, duration)
}
return result
}
let lastRecursiveScrolled = null
let lastX = 0
let lastY = 0
2018-05-26 17:31:27 +02:00
/** Tries to find a node which can be scrolled either x pixels to the right or
* y pixels down among the Elements in {nodes} and children of these Elements.
*
* This function used to be recursive but isn't anymore due to various
* attempts at optimizing the function in order to reduce GC pressure.
*/
export async function recursiveScroll(
x: number,
y: number,
nodes: Element[] = undefined,
ignore: Element[] = [],
2018-05-26 17:31:27 +02:00
) {
let startingFromCached = false
if (!nodes) {
// Check if x and lastX have the same sign and if y and lastY have the same sign
if (lastRecursiveScrolled && (x ^ lastX) >= 0 && (y ^ lastY) >= 0) {
// We're scrolling in the same direction as the previous time so
// let's try to pick up from where we left
startingFromCached = true
nodes = [lastRecursiveScrolled]
} else {
nodes = [document.documentElement]
}
}
2018-05-26 17:31:27 +02:00
let index = 0
let now = performance.now()
do {
let node
do {
node = nodes[index++] as any
} while (ignore.includes(node))
// If node is undefined or if we managed to scroll it
if (!node || (await scroll(x, y, node))) {
// Cache the node for next time and stop trying to scroll
lastRecursiveScrolled = node
return
}
2018-05-26 17:31:27 +02:00
// Otherwise, add its children to the nodes that could be scrolled
nodes = nodes.concat(Array.from(node.children))
if (node.contentDocument) nodes.push(node.contentDocument.body)
} while (index < nodes.length)
// If we reached this part, this means that we couldn't find an element to scroll
// If we started from a cached element, we can try to start again from the
// top of the document and ignore the cached element this time.
// It might be possible to further improve performance by first trying to
// recursiveScroll lastRecursiveScrolled sibling elements and only if
// that fails its parents but this seems unnecessary for now
if (startingFromCached) {
recursiveScroll(
x,
y,
[document.documentElement],
[lastRecursiveScrolled],
)
}
2018-05-26 17:31:27 +02:00
}