Reputation: 221
I'm pretty new to reactive programming (and RxJS) and all these operators are heavy to understand.
Anyway, I've successfully written this function that handles scrolling of the document while dragging something. I now wonder if this can be simplified.
Basically, onMouseDown I need to check the position of the mouse every 10ms and I need the updated clientY
when the mouse moved, thats why I setup an Rx.Oberservable.interval(10)
which I combine with the mouseMove observer. This will scroll the page, whether you move your mouse or not (as intended).
Here's the code:
handleWindowScrollOnDrag() {
var dragTarget = this.getDOMNode()
var scrollTarget = document.body
var wHeight = window.innerHeight
var maxScroll = document.documentElement.scrollHeight - wHeight
// Get the three major events
var mouseup = Rx.Observable.fromEvent(document, 'mouseup');
var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');
var mousedrag = mousedown.flatMap(function (md) {
var y = scrollTarget.scrollTop
var multiplier = 1
// Scroll every 10ms until mouseup when we can
var intervalSource = Rx.Observable.interval(10).takeUntil(mouseup);
// Get actual clientY until mouseup
var movement = mousemove.map(function (mm) {
return {
y: mm.clientY
};
}).takeUntil(mouseup);
return Rx.Observable
.combineLatest(movement, intervalSource, function (s1) {
multiplier = 1
if (s1.y < 100 && y >= 0) {
if (s1.y < 75) multiplier = 3;
if (s1.y < 50) multiplier = 5;
if (s1.y < 25) multiplier = 10;
if (s1.y < 15) multiplier = 20;
y -= (1 * multiplier)
}
if (s1.y > wHeight - 100 && y <= (maxScroll)) {
if (s1.y > wHeight - 75) multiplier = 3;
if (s1.y > wHeight - 50) multiplier = 5;
if (s1.y > wHeight - 25) multiplier = 10;
if (s1.y > wHeight - 15) multiplier = 20;
y += (1 * multiplier)
}
return {
y: y
};
});
});
// Update position
this.subscription = mousedrag.subscribe(function (pos) {
document.body.scrollTop = pos.y
});
},
Upvotes: 2
Views: 2021
Reputation: 18663
You can remove the extra takeUntil
s you only need one on the movement
stream, since the combineLatest
operator will clean up and dispose of all the underlying subscriptions when it completes.
Next, you can remove some extra state by using .scan
to manage accumulated state instead of closure variables.
You can also simplify the event body by simply passing the mouse move's y
value instead of incurring the overhead of an object allocation in both the map
and the combineLatest
operators.
Finally, I would change to use .withLatestFrom
instead of combineLatest
. Since you are already essentially polling at 100 fps with interval combineLatest
would emit even faster if the mouse was moving at the same time.
As a side note, while I am not terribly familiar with how DOM scrolling works (and I don't actually know how well your code works in the wild), spamming the page's scroll value faster than the actual render rate of the page seems like overkill. It might be better to lower the interval rate and use something like jQuery.animate
to smooth the scrolling in between.
;tldr
handleWindowScrollOnDrag() {
var dragTarget = this.getDOMNode()
var scrollTarget = document.body;
var wHeight = window.innerHeight;
var maxScroll = document.documentElement.scrollHeight - wHeight;
// Get the three major events
var mouseup = Rx.Observable.fromEvent(document, 'mouseup');
var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');
var mousedrag = mousedown.flatMap(function (md) {
// Scroll every 10ms until mouseup when we can
var intervalSource = Rx.Observable.interval(10, Rx.Scheduler.requestAnimationFrame);
return intervalSource
.takeUntil(mouseup)
.withLatestFrom(mousemove, function (s1, s2) {
return s2.clientY;
})
.scan(scrollTarget.scrollTop, function(y, delta) {
var multiplier = 1;
if (delta < 100 && y >= 0) {
if (delta < 75) multiplier = 3;
if (delta < 50) multiplier = 5;
if (delta < 25) multiplier = 10;
if (delta < 15) multiplier = 20;
y -= (1 * multiplier);
}
if (delta > wHeight - 100 && y <= (maxScroll)) {
if (delta > wHeight - 75) multiplier = 3;
if (delta > wHeight - 50) multiplier = 5;
if (delta > wHeight - 25) multiplier = 10;
if (delta > wHeight - 15) multiplier = 20;
y += (1 * multiplier);
}
return y;
});
});
// Update position
this.subscription = mousedrag.subscribe(function (pos) {
document.body.scrollTop = pos;
});
},
Edit
A mistake in the original actually allows one to simplify the code even further. You can remove the movement
stream all together (and with it an extra call to map
) by passing mousemove
to withLatestFrom
and using that result selector to grab the clientY
Additionally I added where you would likely want to add a scheduler if you were trying to synchronize interval
with the render loop of the page, though I would still say you probably want to use 15ms (60fps) rather than 10ms, it again comes down to how the scroll rendering is done.
If setting the scroll position will only be updated at the end of each render loop then it will work fine. However, if it greedily recomputes after every set
then it would be better to have an intermediary like React
to write to Virtual DOM first rather than flushing all changes directly to the DOM.
Upvotes: 3