Reputation: 2296
I try to implement drag and drop in RxJS. I have a DOM node with id draggable
that can be dragged around. By using the standard procedure drag and drop works as expected.
But I tried to enhance drag and drop and this is where things get complicated. I attempt to change the background color of the element once dragging starts and change it back once it's dropped.
In my approach I'm using switchMap
to map the results of the mouse move event into my observable which is triggered by the mouse down event. But since I use the mouse up event to complete the switchMap
ed observable (mm$
in the example below) I have no chance to get notified about the completion event of the inner observable except when I'm subscribing to it within the switchMap
operator.
I know that subscribing within an operator is far from good practice and might lead to memory leaks. But what else can I do? How can this be done better?
Fiddle: https://jsfiddle.net/djwfyxs5/
const target = document.getElementById('draggable');
const mouseup$ = Observable.fromEvent(document, 'mouseup');
const mousedown$ = Observable.fromEvent(target, 'mousedown');
const mousemove$ = Observable.fromEvent(document, 'mousemove');
const move$ = mousedown$
.switchMap(md => {
md.target.style.backgroundColor = 'yellow';
const {offsetX: startX, offsetY: startY} = md;
const mm$ = mousemove$
.map(mm => {
mm.preventDefault();
return {
left: mm.clientX - startX,
top: mm.clientY - startY
};
})
.takeUntil(mouseup$);
// Can the next line be avoided?
mm$.subscribe(null, null, () => {
md.target.style.backgroundColor = 'purple';
});
return mm$;
});
move$.subscribe((pos) => {
target.style.top = pos.top + 'px';
target.style.left = pos.left + 'px';
});
Upvotes: 1
Views: 1136
Reputation: 2296
I've struggled further with the problem to find a solution. In my attempt I use a helper observable that combines mousedown
and mouseup
events. By combining them with the combineLatest
operator the latest value of the mousedown event can be accessed which contains the item that have been clicked (target).
This allows me to set the color correctly without the subscription in a temporary observable as seen in the issue. My solution can be accessed in this fiddle.
I'm not sure if this can be done better/with less code using the same idea. I would be happy to see an improved implementation if possible.
Full code:
const targets = document.getElementsByClassName('draggable');
const arrTargets = Array.prototype.slice.call(targets);
const mouseup$ = Rx.Observable.fromEvent(document, 'mouseup');
const mousedown$ = Rx.Observable.fromEvent(targets, 'mousedown');
const mousemove$ = Rx.Observable.fromEvent(document, 'mousemove');
// md -------x------------------------------------------
// mm xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// mu -----------------------------------------x--------
// move$ -------xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx---------
// s$ -------x---------------------------------x--------
const s$ = Rx.Observable.combineLatest(
mousedown$,
mouseup$.startWith(null), // start immediately
(md, mu) => {
const { target } = md; // Target is always the one of mousedown event.
let type = md.type;
// Set type to event type of the newer event.
if (mu && (mu.timeStamp > md.timeStamp)) {
type = mu.type;
}
return { target, type };
}
);
const move$ = mousedown$
.switchMap(md => {
const { offsetX: startX, offsetY: startY } = md;
const mm$ = mousemove$
.map(mm => {
mm.preventDefault();
return {
left: mm.clientX - startX,
top: mm.clientY - startY,
event: mm
};
})
.takeUntil(mouseup$);
return mm$;
});
Rx.Observable.combineLatest(
s$, move$.startWith(null),
(event, move) => {
let newMove = move || {};
// In case we have different targets for the `event` and
// the `move.event` variables, the user switched the
// items OR the user moved the mouse too fast so that the
// event target is the document.
// In case the user switched to another element we want
// to ensure, that the initial position of the currently
// selected element is used.
if (move && move.event.target !== event.target && arrTargets.indexOf(move.event.target) > -1) {
const rect = event.target.getBoundingClientRect();
newMove = {
top: rect.top, // + document.body.scrollTop,
left: rect.left // + document.body.scrollLeft
};
}
return { event, move: newMove }
}
)
.subscribe(action => {
const { event, move } = action;
if (event.type === 'mouseup') {
event.target.style.backgroundColor = 'purple';
event.target.classList.remove('drag');
} else {
event.target.style.backgroundColor = 'red';
event.target.classList.add('drag');
}
event.target.style.top = move.top + 'px';
event.target.style.left = move.left + 'px';
});
Upvotes: 0
Reputation: 2796
I answered a similar question here: RxJs: Drag and Drop example : add mousedragstart
It should be reasonably straightforward to adapt the answer to your purpose, as the streams still contain the events which expose the elements for which they were raised.
Upvotes: 2