Creating an observable 'completed' event in RxJS - javascript

Given: the reactive extensions drag and drop example , how would you subscribe to just a drop event?
I have modified the code to subscribe to a 'completed' callback, but it does not complete.
(function (global) {
function main () {
var dragTarget = document.getElementById('dragTarget');
var $dragTarget = $(dragTarget);
// 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
.filter(function(md){
//console.log(md.offsetX + ", " + md.offsetY);
return md.offsetX <= 100
||
md.offsetY <= 100;
})
.flatMap(function (md) {
// calculate offsets when mouse down
var startX = md.offsetX, startY = md.offsetY;
// Calculate delta with mousemove until mouseup
return mousemove.map(function (mm) {
mm.preventDefault();
return {
left: mm.clientX - startX,
top: mm.clientY - startY
};
}).takeUntil(mouseup);
});
// Update position
var subscription = mousedrag.subscribe(
function (pos) {
dragTarget.style.top = pos.top + 'px';
dragTarget.style.left = pos.left + 'px';
},
function(errorToIgnore) {},
function() { alert('drop');});
}
main();
}(window));
I have read that hot observables, such as those that have been created from mouse events, never 'complete'. Is this correct? How can I otherwise get a callback on 'drop'?

Something like this should do the trick.
(function (global) {
function main () {
var dragTarget = document.getElementById('dragTarget');
// 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 drop = mousedown
.selectMany(
Rx.Observable
.concat(
[
mousemove.take(1).ignoreElements(),
mouseup.take(1)
]
)
);
}
main();
}(window));
Edit:
If you think of an observable as an asynchronous function which yields multiple values, and then possibly completes or errors, you'll immediately recognize that there can only be one completion event.
When you start composing multiple function, the outer-most function still only completes once, even if that function contains multiple functions inside of it. So even though the total number of "completions" is 3, the outer-most function still only completes once.
Basically, that means if the outer-most function is suppose to return a value each time a drag completes, you need a way of actually doing that. You need to translate the drag completion into an "onNext" event for the outer-most observable.
ANY which way you can do that is going to get you what you want. Maybe that's the only kind of events that the outer-most function returns, or maybe it also returns drag starts and moves, but so long as it returns the drag completions, you'll end up with what you need (even if you have to filter it later).
The example I've given above is just one way to return the drag drops in the outer-most observable.

OP's link to the official example was down, it's here:
https://github.com/Reactive-Extensions/RxJS/blob/master/examples/dragndrop/dragndrop.js
There are two solutions to the original question, using either the primitive mouse events expanding upon the official example, or the native HTML5 drag and drop events.
Composing from 'mouseup', 'mousedown' and 'mousemove'
First we use mousedown1.switchMapTo(mousemove.takeUntil(mouseup).take(1)) to get the 'drag start' stream. switchMapTo (see doc) uses a combination of flattening (mapping 'mousedown1' to each of the first emit of 'mouse moving until mouse up', aka 'dragging') and switch (see doc), giving us the latest mousedown1 emit followed by dragging, i.e. not just any time you click your mouse on the box.
Another 'mousedown' stream mousedown2 is used to compose a mousedrag stream so we can constantly render the box while dragging. mousedown1 above is prioritised over mousedown2. See https://stackoverflow.com/a/35964479/232288 for more information.
Once we have our 'drag start' stream we can get the 'drag stop' stream via: mousedragstart.mergeMapTo(mouseup.take(1)). mergeMapTo (see doc) maps 'mousedragstart' to each of the first emit of 'mouseup', giving us the latest 'mousedragstart' followed by 'mouseup', which is essentially 'drag stop'.
The demo below works with RxJS v5:
const { fromEvent } = Rx.Observable;
const target = document.querySelector('.box');
const events = document.querySelector('#events');
const mouseup = fromEvent(target, 'mouseup');
const mousemove = fromEvent(document, 'mousemove');
const [mousedown1, mousedown2] = prioritisingClone(fromEvent(target, 'mousedown'));
const mousedrag = mousedown2.mergeMap((e) => {
const startX = e.clientX + window.scrollX;
const startY = e.clientY + window.scrollY;
const startLeft = parseInt(e.target.style.left, 10) || 0;
const startTop = parseInt(e.target.style.top, 10) || 0;
return mousemove.map((e2) => {
e2.preventDefault();
return {
left: startLeft + e2.clientX - startX,
top: startTop + e2.clientY - startY
};
}).takeUntil(mouseup);
});
// map the latest mouse down emit to the first mouse drag emit, i.e. emits after pressing down and
// then dragging.
const mousedragstart = mousedown1.switchMapTo(mousemove.takeUntil(mouseup).take(1));
// map the mouse drag start stream to first emit of a mouse up stream, i.e. emits after dragging and
// then releasing mouse button.
const mousedragstop = mousedragstart.mergeMapTo(mouseup.take(1));
mousedrag.subscribe((pos) => {
target.style.top = pos.top + 'px';
target.style.left = pos.left + 'px';
});
mousedragstart.subscribe(() => {
console.log('Dragging started');
events.innerText = 'Dragging started';
});
mousedragstop.subscribe(() => {
console.log('Dragging stopped');
events.innerText = 'Dragging stopped';
});
function prioritisingClone(stream$) {
const first = new Rx.Subject();
const second = stream$.do(x => first.next(x)).share();
return [
Rx.Observable.using(
() => second.subscribe(() => {}),
() => first
),
second,
];
}
.box {
position: relative;
width: 150px;
height: 150px;
background: seagreen;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>
<div class="box"></div>
<h3 id="events"></h3>
Composing from HTML5 native 'dragstart', 'dragover' and 'drop'
The native events make things a little easier. Just make sure the dragover observer calls e.preventDefault() so the body element can become a valid drop zone. See more info.
We use switchMap (see doc) in a similar fashion as how switchMapTo is used above: flatten and map the latest 'dragstart' to the latest 'drop' to get our 'drag then drop' stream.
The difference is only when the user drops the div do we update the position.
const { fromEvent } = Rx.Observable;
const target = document.querySelector('.box');
const events = document.querySelector('#events');
const dragstart = fromEvent(target, 'dragstart');
const dragover = fromEvent(document.body, 'dragover');
const drop = fromEvent(document.body, 'drop');
const dragthendrop = dragstart.switchMap((e) => {
const startX = e.clientX + window.scrollX;
const startY = e.clientY + window.scrollY;
const startLeft = parseInt(e.target.style.left, 10) || 0;
const startTop = parseInt(e.target.style.top, 10) || 0;
// set dataTransfer for Firefox
e.dataTransfer.setData('text/html', null);
console.log('Dragging started');
events.innerText = 'Dragging started';
return drop
.take(1)
.map((e2) => {
return {
left: startLeft + e2.clientX - startX,
top: startTop + e2.clientY - startY
};
});
});
dragover.subscribe((e) => {
// make it accepting drop events
e.preventDefault();
});
dragthendrop.subscribe((pos) => {
target.style.top = `${pos.top}px`;
target.style.left = `${pos.left}px`;
console.log('Dragging stopped');
events.innerText = 'Dragging stopped';
});
.box {
position: relative;
width: 150px;
height: 150px;
background: seagreen;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>
<div class="box" draggable="true"></div>
<h3 id="events"></h3>

Related

When the drag event is fired in Firefox, the mouse coordinates are (0,0)

I want to make a mobile phone theme effect on the browser, and I need to make the icon get the current mouse position when it is dragged. In order to judge whether the user wants to insert the currently dragged icon at the mouse position, but I use the event object (#drag($event) ) of the drag method in the Firefox browser to get the mouse coordinates (event. pageX, event.screenX), it shows (0,0) or a fixed value, but when I use Google Chrome, the above situation does not occur, it immediately gives me the coordinates of the current mouse. Regarding the problem of the value of layerXY in the picture, this value will only be updated once at the beginning of dragging, and will not change at the rest of the time. Since I personally like to use the Firefox browser, I want to solve this problem, can anyone help me? Or give me some other suggestions to implement this function (my English is not very good, from google translate)
You could update the mouse coordinate on a global variable when the mouse moves so that it will be ready for you when mouse is down.
let drag = document.querySelector('.note');
var pageX, pageY
drag.onmousedown = function(e) {
let coord = getCoord(drag);
let shiftX = pageX - coord.left;
let shiftY = pageY - coord.top;
drag.style.position = 'absolute';
document.body.appendChild(drag);
moveNote(e);
drag.style.zIndex = 1000;
function moveNote(e) {
drag.style.left = pageX - shiftX + 'px';
drag.style.top = pageY - shiftY + 'px';
var position = {
x: drag.style.left,
y: drag.style.top
}
}
document.onmousemove = function(e) {
moveNote(e);
};
drag.onmouseup = function() {
document.onmousemove = null;
drag.onmouseup = null;
};
}
function getCoord(elem) {
let main = elem.getBoundingClientRect();
return {
top: main.top,
left: main.left
};
}
window.onload = function() {
document.addEventListener("mousemove", function(e) {
pageX = e.pageX
pageY = e.pageY
});
drag.style.position = 'absolute';
document.body.appendChild(drag);
drag.style.display = 'block'
}
.note {
width: 50px;
height: 50px;
background: red;
display: none;
}
<div class="note"></div>

Custom drag drop implementation

I am rolling out a custom drag drop implementation which is a simplified version of the Dragula library. Here are my fiddles:
With Dragula: http://jsfiddle.net/8m4jrfwn/
With my custom drag drop implementation: http://jsfiddle.net/7wLtojs4/1/
The issue I am seeing is that my custom drag and drop implementation is a bit to free-form. I would like for the user to only be able to drop on the top or bottom of another div with the class box. How can I make this possible? Code here:
function handleMouseDown(e) {
window.dragging = {};
dragging.pageX = e.pageX;
dragging.pageY = e.pageY;
dragging.elem = this;
dragging.offset = $(this).offset();
$('body')
.on('mouseup', handleMouseUp)
.on('mousemove', handleDragging);
}
function handleDragging(e) {
let left = dragging.offset.left + (e.pageX - dragging.pageX);
let top = dragging.offset.top + (e.pageY - dragging.pageY);
$(dragging.elem)
.offset({top: top, left: left});
}
function handleMouseUp() {
$('body')
.off('mousemove', handleDragging)
.off('mouseup', handleMouseUp);
}
let boxElement = '.item';
$(boxElement).mousedown(handleMouseDown);
I does not see any logic here about what will happen on mouseup event. Seems you need detect on mouseup new order of elements and then exchange their positions.
For this you should know positions of all of them. On mouseup you can reorder them based on new positions.
In my realization(https://jsfiddle.net/jaromudr/c99oueg5/) I used next logic:
onMove(draggable) {
const sortedDraggables = this.getSortedDraggables()
const pinnedPositions = sortedDraggables.map((draggable) => draggable.pinnedPosition)
const currentIndex = sortedDraggables.indexOf(draggable)
const targetIndex = indexOfNearestPoint(pinnedPositions, draggable.position, this.options.radius, getYDifference)
if (targetIndex !== -1 && currentIndex !== targetIndex) {
arrayMove(sortedDraggables, currentIndex, targetIndex)
this.bubling(sortedDraggables, draggable)
}
}
bubling(sortedDraggables, currentDraggable) {
const currentPosition = this.startPosition.clone()
sortedDraggables.forEach((draggable) => {
if (!draggable.pinnedPosition.compare(currentPosition)) {
if (draggable === currentDraggable && !currentDraggable.nativeDragAndDrop) {
draggable.pinnedPosition = currentPosition.clone()
} else {
draggable.pinPosition(currentPosition, (draggable === currentDraggable) ? 0 : this.options.timeExcange)
}
}
currentPosition.y = currentPosition.y + draggable.getSize().y + this.verticalGap
})
}
where on move we detect if position in list was changed and move other draggables to their new positions.
On drag end we just move currentDraggable to pinnedPosition

Observer check when mousemove stops

I want to execute function when a mouse move ends inside mouseDown Observer. But onComplete function doesn't execute, when i letdown my mouse. Any suggestions?
var split = $('.drag');
var parent = $('.Container');
var mouseDowns = Rx.Observable.fromEvent(split, "mousedown");
var parentMouseMoves = Rx.Observable.fromEvent(parent, "mousemove");
var parentMouseUps = Rx.Observable.fromEvent(parent, "mouseup");
var drags = mouseDowns.flatMap(function(e){
return parentMouseMoves.takeUntil(parentMouseUps);
});
drags.subscribe(
function(e) {
var $containerWidth = $('.Container').width();
var clientX = $containerWidth - e.clientX;
if (clientX >= 50 && e.clientX >= 50) {
$('.left').css('right', clientX);
$('.right').css('width', clientX);
}
},
function(error) {
console.log(error);
},
function() {
console.log('finished');
});
jsbin.com url
If you want to detect drag event end, a.k.a. drop, then something like this should do the trick:
var drop = mouseDowns.selectMany(
Rx.Observable.concat([
parentMouseMoves.take(1).ignoreElements(),
parentMouseUps.take(1)
])
);
drop.subscribe(function(e) {
console.log('finished');
});
If the outer-most function is suppose to return a value each time a drag completes, you need to convert the drag completion into an "onNext" event for the outer-most observable.

Do JavaScript event handlers get overwritten?

I am curious if binding an event to an object multiple times cause problems by stacking multiple event listeners on top of each other or do they override each other? For example if I have an attachHandlers function
function _attachHandlers() {
// Slider hover
var isDown = false;
$('.ui-slider-handle').mousedown(function () {
// Create tool tip
isDown = true;
var tt = $(document.createElement('span')).addClass('sk-tooltip');
var handle = $(this);
var left = parseInt(handle.css('left'));
var val = _convertSliderValue(Math.round(left / 5 / 14.2857));
tt.text(val);
handle.append(tt);
var newLeft = (handle.outerWidth() - tt.outerWidth()) / 2;
tt.css({
'left': newLeft
});
$(document).mousemove(function (event) {
var left = parseInt(handle.css('left'));
var val = _convertSliderValue(Math.round(left / 5 / 14.2857));
tt.text(val);
var newLeft = (handle.outerWidth() - tt.outerWidth()) / 2;
tt.css({
'left': newLeft
});
});
});
$(document).mouseup(function () {
if (isDown) {
$(document).unbind('mousemove');
$(this).find('.sk-tooltip').remove()
isDown = false;
}
});
}
I need to re-attach these handlers at some point in my code and therefore I was just going to call _attachHandlers() to re-bind them, however, I will also be re-binding the document listener for the mouseup event every time this happens. Therefore, is it alright to do this and will the event handler just get overwritten everytime or do I have to unbind the handler first before it can be re-bound?

Raphael: How to unbind the mousemove function on mouseup?

I am drawing a line with Raphael. I have a mousedown event where I store the starting position. While the mouse is down, if the user moves it, I have a mousemove event where I keep drawing the line as the mouse moves.
Now when the mouse button is released, the line should stop. This does not happen and line keeps going on if the mouse moves even though button is released. This line must stop on mouseup. If the user does mousedown again, it should begin a new line.
I have tried many combinations with the unmouse* events, but I am missing something here.
JSFiddle at: http://jsfiddle.net/zaphod013/P33FA/5/
(This is my first date with JS/Raphael. So if you think I am totally off track here, please tell me so)
var g_masterPaper;
var g_startX;
var g_startY;
var g_line;
function initDrawing() {
g_masterPaper = Raphael(10,10,700,500);
var masterBackground = g_masterPaper.rect(10,10,600,400);
masterBackground.attr("fill", "#eee");
var drawit = function(event) {
x = event.pageX - 10;
y = event.pageY - 10;
var linepath = ("M"+g_startX+" "+g_startY+" L"+x+" "+y);
g_line.attr("path", linepath);
};
var startit = function (event) {
g_startX = event.pageX - 10;
g_startY = event.pageY - 10;
g_line = g_masterPaper.path("M"+g_startX+" "+g_startY+" L"+g_startX+" "+g_startY);
masterBackground.mousemove(drawit);
};
masterBackground.mousedown(startit);
masterBackground.mouseup(function (event) {
this.unmousedown(startit);
this.unmousemove(drawit);
});
return g_masterPaper;
}
window.onload = function () {
var paper=initDrawing();
paper.text(15,475,'Use your mouse to draw.').attr({fill:'#ff0000', 'font-size':24, 'stroke-width':1,'text-anchor':'start' });
}
I think you're on track and mostly looks fine, I would possibly just simplify your handlers, and not keep removing/adding them, it keeps handlers hard to track and debug. So I would just have one handler for down/up/move...
Edit:
jsfiddle here or here which gets around other elements capturing the mouseup event and stopping it working properly.
var drawit = function(event) {
event.preventDefault();
x = event.pageX - 10;
y = event.pageY - 10;
var linepath = ("M"+g_startX+" "+g_startY+" L"+x+" "+y);
if( g_line ) { g_line.attr("path", linepath) };
};
var startit = function (event) {
event.preventDefault();
g_startX = event.pageX - 10;
g_startY = event.pageY - 10;
g_line = g_masterPaper.path("M"+g_startX+" "+g_startY+" L"+g_startX+" "+g_startY);
};
var finish = function ( event ) {
g_line = '';
}

Categories