58
loading...
This website collects cookies to deliver better user experience
drag start
happens whenever we press mouse down on a draggable item. Following that each time we move a cursor a drag move
event should be emitted. Drag move should continue, but only until we release the mouse button (mouse up event).mousedown
- for starting the draggingmousemove
- for moving the dragged elementmouseup
- for ending the dragging (dropping an element)import { fromEvent } from 'rxjs'
const draggableElement = document.getElementById('dragMe');
const mouseDown$ = fromEvent(draggableElement, 'mousedown');
const mouseMove$ = fromEvent(draggableElement, 'mousemove');
const mouseUp$ = fromEvent(draggableElement, 'mouseup');
import { switchMap, takeUntil } from 'rxjs/operators';
const dragStart$ = mouseDown$;
const dragMove$ = dragStart$.pipe( // whenever we press mouse down
switchMap(() => mouseMove$).pipe( // each time we move a cursor
takeUntil(mouseUp$) // but only until we release the mouse button
),
);
dragMove$
Observable so that we know how far we drag the element. For that, we can use the value emitted by dragStart$
, and compare it with each value emitted by mouseMove$
:const dragMove$ = dragStart$.pipe(
switchMap(start =>
mouseMove$.pipe(
// we transform the mouseDown and mouseMove event to get the necessary information
map(moveEvent => ({
originalEvent: moveEvent,
deltaX: moveEvent.pageX - start.pageX,
deltaY: moveEvent.pageY - start.pageY,
startOffsetX: start.offsetX,
startOffsetY: start.offsetY
})),
takeUntil(mouseUp$)
)
),
);
subscribe
it to perform any action.dragMove$.subscribe(move => {
const offsetX = move.originalEvent.x - move.startOffsetX;
const offsetY = move.originalEvent.y - move.startOffsetY;
draggableElement.style.left = offsetX + 'px';
draggableElement.style.top = offsetY + 'px';
});
mouseMove$
and mouseUp$
events are listening on the dragged element itself. If the mouse moves too fast, the cursor can leave the dragged element, and then we will stop receiving the mousemove
event. The easy solution to this is to target mouseMove$
and mouseUp$
to the document
so that we receive all the mouse events even if we leave the dragged element for a moment.const mouseMove$ = fromEvent(document, 'mousemove');
const mouseUp$ = fromEvent(document, 'mouseup');
const mouseMove$ = fromEvent(document, 'mousemove');
const mouseUp$ = fromEvent(document, 'mouseup');
const draggableElement = document.getElementById('dragMe');
createDraggableElement(draggableElement);
function createDraggableElement(element) {
const mouseDown$ = fromEvent(element, 'mousedown');
const dragStart$ = mouseDown$;
const dragMove$ = dragStart$.pipe(
switchMap(start =>
mouseMove$.pipe(
map(moveEvent => ({
originalEvent: moveEvent,
deltaX: moveEvent.pageX - start.pageX,
deltaY: moveEvent.pageY - start.pageY,
startOffsetX: start.offsetX,
startOffsetY: start.offsetY
})),
takeUntil(mouseUp$)
)
)
);
dragMove$.subscribe(move => {
const offsetX = move.originalEvent.x - move.startOffsetX;
const offsetY = move.originalEvent.y - move.startOffsetY;
element.style.left = offsetX + 'px';
element.style.top = offsetY + 'px';
});
}
appDiv.innerHTML = `
<h1>RxJS Drag and Drop</h1>
<div class="draggable"></div>
<div class="draggable"></div>
<div class="draggable"></div>
`;
const draggableElements = document.getElementsByClassName('draggable');
Array.from(draggableElements).forEach(createDraggableElement);
dragStart$
and dragMove$
observables. We can use those directly to start emitting mydragstart
and mydragmove
events on the element accordingly. I've added a my
prefix to make sure I don't collide with any native event.import { tap } from 'rxjs/operators';
dragStart$
.pipe(
tap(event => {
element.dispatchEvent(
new CustomEvent('mydragstart', { detail: event })
);
})
)
.subscribe();
dragMove$
.pipe(
tap(event => {
element.dispatchEvent(
new CustomEvent('mydragmove', { detail: event })
);
})
)
.subscribe();
tap
function. This is an approach I recommend as this allows us to combine multiple observable streams into one and call subscribe
only once:import { combineLatest } from 'rxjs';
combineLatest([
dragStart$.pipe(
tap(event => {
element.dispatchEvent(
new CustomEvent('mydragstart', { detail: event })
);
})
),
dragMove$.pipe(
tap(event => {
element.dispatchEvent(
new CustomEvent('mydragmove', { detail: event })
);
})
)
]).subscribe();
mydragend
. This event should be emitted as the last event of the mydragmove
event sequence. We can again use the RxJS operator to achieve such behavior.const dragEnd$ = dragStart$.pipe(
switchMap(start =>
mouseMove$.pipe(
map(moveEvent => ({
originalEvent: moveEvent,
deltaX: moveEvent.pageX - start.pageX,
deltaY: moveEvent.pageY - start.pageY,
startOffsetX: start.offsetX,
startOffsetY: start.offsetY
})),
takeUntil(mouseUp$),
last(),
)
)
);
combineLatest([
dragStart$.pipe(
tap(event => {
element.dispatchEvent(
new CustomEvent('mydragstart', { detail: event })
);
})
),
dragMove$.pipe(
tap(event => {
element.dispatchEvent(new CustomEvent('mydragmove', { detail: event }));
})
),
dragEnd$.pipe(
tap(event => {
element.dispatchEvent(new CustomEvent('mydragend', { detail: event }));
})
)
]).subscribe();
Array.from(draggableElements).forEach((element, i) => {
element.addEventListener('mydragstart', () =>
console.log(`mydragstart on element #${i}`)
);
element.addEventListener('mydragmove', event =>
console.log(
`mydragmove on element #${i}`,
`delta: (${event.detail.deltaX}, ${event.detail.deltaY})`
)
);
element.addEventListener('mydragend', event =>
console.log(
`mydragend on element #${i}`,
`delta: (${event.detail.deltaX}, ${event.detail.deltaY})`
)
);
});