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()
|
|
|
|
|
2018-05-29 20:43:31 +02:00
|
|
|
let opts = { smooth: null, duration: null }
|
2018-05-26 17:31:27 +02:00
|
|
|
async function getSmooth() {
|
2018-05-29 20:43:31 +02:00
|
|
|
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() {
|
2018-05-29 20:43:31 +02:00
|
|
|
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
|
|
|
}
|
2018-05-29 20:43:31 +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)
|
2018-05-29 20:43:31 +02:00
|
|
|
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)
|
2018-05-29 20:43:31 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-05-29 20:43:31 +02:00
|
|
|
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,
|
2018-05-29 20:43:31 +02:00
|
|
|
nodes: Element[] = undefined,
|
|
|
|
ignore: Element[] = [],
|
2018-05-26 17:31:27 +02:00
|
|
|
) {
|
2018-05-29 20:43:31 +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 {
|
2018-05-29 20:43:31 +02:00
|
|
|
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)
|
2018-05-29 20:43:31 +02:00
|
|
|
// 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
|
|
|
}
|