I m studying cyclejs and having some trouble dealing with how to make a div movabble.
To start, I have isolated 3 events that I have to compute
mousedown
mouseup
mousemove
The aim is to produce the move when map is down AND mousemove, and stop when I mouseup
Here's what I've got :
import {div} from '#cycle/dom'
import xs from 'xstream'
const getStyle = left => top => {
return ({
style: {
position: 'absolute',
left: left + 'px',
top: top + 'px',
backgroundColor: '#FF0000',
cursor: 'pointer'
}
})
}
function intent(DOMSources) {
const marble$ = DOMSources.select('.marble')
const marbleDown$ = marble$.events('mousedown')
const marbleMove$ = marble$.events('mousemove')
const marbleUp$ = marble$.events('mouseup')
return {marbleDown$, marbleUp$, marbleMove$}
}
function model({marbleDown$, marbleUp$, marbleMove$}) {
return xs.combine(marbleDown$, marbleMove$)
.map(([marbleDown, marbleMove]) => marbleMove)
.map(ev => ({x: ev.pageX, y: ev.pageY}))
.endWhen(marbleUp$)
}
function view(state$) {
return state$
.startWith({x: 0, y: 0})
.map(value => div('.marble', getStyle(value.x)(value.y), 'Move me ! ' + value.x + ' - ' + value.y))
}
export function Marble(sources) {
const changes$ = intent(sources.DOM)
const state$ = model(changes$)
const vTree$ = view(state$)
return {DOM: vTree$}
}
My problem seems to appear on the model part. When I enter the the div and mousedown and it, and moving the element, I m only able to move the element to down and right, not to top and left.
My second problem is that when I leave the bouton with my mouse, it continues to move when I refocus on it.
It seems that I m missing something in a bad way.
A gif is better that a thousand words :
Your problem is, your mouse is outside of div when you move mouse to left. You can solve this problem as this:
function between(first, second) {
return (source) => first.mapTo(source.endWhen(second)).flatten()
}
function model({marbleDown$, marbleUp$, marbleMove$}) {
return xs.combine(marbleDown$, marbleMove$)
.map(([marbleDown, marbleMove]) => ({x: marbleMove.pageX - marbleDown.layerX, y: marbleMove.pageY - marbleDown.layerY}))
.compose(between(marbleDown$, marbleUp$))
}
But you will have problem with mouseUp event. Because you left your div mouseUp event wait listener on your div element, so you need put your div on container. And take mouseUp listener to this container element. For example container can be body element.
const marbleUp$ = DOMSources.select('body').events('mouseup')
I recommend to you css style for marble div 'user-selection': 'none'. This will deactive selection mode when you click on marble.
Related
I have a widget container where if I go to the right side, a dragger selector appears (mouseover).
I need to that dragger follow the mouse position in axis Y inside the widget container limits.
I have tried a lot of things but I'm not able to get the right position for the dragger.
rh.addEventListener('mouseover', (event) => {
const draggerElement = event.target.parentNode.querySelector('.widget-dragger')
if (draggerElement.style.opacity !== 1) {
draggerElement.style.top = `${event.pageY - (event.target.offsetHeight / 2)}px`
draggerElement.style.opacity = 1
}
})
jsfiddle: https://jsfiddle.net/rt0s8p7h/1/
Captures:
This is the rh element:
It has 100% height of the widget container.
Dragger has absolute position inside the widget container.
Widget Container has relative position, so the dragger is limited inside the container.
Solution here: https://jsfiddle.net/rt0s8p7h/3/
rh.addEventListener('mouseover', (event) => {
const draggerElement = event.target.parentNode.querySelector(".widget-dragger")
if (draggerElement.style.opacity !== 1) {
const rhRect = event.target.getBoundingClientRect()
draggerElement.style.top = `${event.clientY - rhRect.top - draggerElement.offsetHeight/2}px`
draggerElement.style.opacity = 1
}
})
Also works for mousemove to follow the mouse cursor.
I have a complex UI component with custom Drag & Drop behaviour and custom event code of several hundred lines, that basically works via regular MouseEvents.
I need to add additional functionality so that a user can Drag & Drop items from inside the component to outside the component. The "outside" works via regular HTML5 Drag & Drop events, meaning there is an element that already listens to the drop event, etc. Now, i just need to trigger the whole Drag & Drop chain, so that the drop zone element notices the event and during the drag operation i have the regular look drag image.
Simply setting my draggable item to draggable="true", does not work as the dragstart event is not thrown, it is prevented by our custom event code inside the component with event.preventDefault(). So my idea was to manually trigger dragstart events whenever the user wants to drag the item outside of the boundary of the component.
My current status is, that my onMoveStart() triggers the dragstart event, once it realizes the user wants to drag the item outside of the component. The onMove() triggers regular drag events while moving the mouse, the onMoveEnd() triggers the corresponding dragend / drop events. A somewhat similar and very boiled down version can be seen here: Fiddle.
Problem: A dragstart is properly triggered when dragging an item outside of the component, a drag event is properly triggered while regular mouse moving, the dragend event also works. The dropzone element however never receives the drop event. Additionally, there is no dragging image during the drag operation, which makes this look not working to the user. The datatransfer that i manually setup always has effectAllowed and dropOperation set to none even though i manually set them to proper values.
How can i get this working? How to implement the Drag & Drop behaviour manually? How to set the effectAllowed and dropEffect properties on DataTransfer manually so that i have a proper dragging effect without them being always overwritten to none?
Based on the code you provided, I produced something that built around it. From your post and my understanding, I believe there are 2 problems or maybe 3 that you are trying to solve
I start with dragging image. So I just clone the element that we want it to appear as dragging, set it with proper attributes
function draggingImage(el) {
let image = draggable.cloneNode(true);
image.setAttribute("id", "drag-image");
image.style.width = el.offsetWidth + 'px'
image.style.height = el.offsetHeight + 'px'
image.style.position = 'fixed'
image.style.left = el.getBoundingClientRect().left + 'px';
image.style.top = el.getBoundingClientRect().top + 'px';
image.style.background = getComputedStyle(draggable).backgroundColor
image.style.opacity = .5
let div = document.body.appendChild(image);
}
as for now, the dragging image is static, as expected, and we want it to move as our mouse move thus the dragging effect
let image = document.getElementById('drag-image')
image.style.left = image.getBoundingClientRect().left + e.movementX + 'px'
image.style.top = image.getBoundingClientRect().top + e.movementY + 'px'
while our mousemove with wasdragging = true, it's a good time to detect what's underneath our cursor, are we within the dropzone or outside
function getDropzone(e, left, top, id) {
// let rect1 = document.getElementById(id).getBoundingClientRect();
let rect1 = drop.getBoundingClientRect()
// to detect the overlap of mouse into the dropzone, as alternative of mouseover
var overlap = !(rect1.right < left ||
rect1.left > left ||
rect1.bottom < top ||
rect1.top > top)
return overlap
}
we can add the above function within onDrag function. The getDropzone function return true or false, and that's all needed. From there we can execute our logic or styling accordingly.
I also add global variable called as data, then a function that work almost similar to dataTransfer concept, naively.
let data = {}
function manualSetData(variable, v) {
data[variable] = v
}
with this, we can just get the data that we set with this function in ondragend
let getData = data[variable]
so this is DnD from scratch in a nutshell
const draggable = document.querySelector("#draggable");
const drop = document.querySelector("#drop");
const condition = true;
let wasDragging = false;
let allowedDrop = false
let data = {}
draggable.addEventListener("mousedown", onMoveStart)
document.addEventListener("mousemove", onMove)
document.addEventListener("mouseup", onMoveEnd)
function onMoveStart(e) {
event.preventDefault();
event.stopPropagation();
let el = e.target
// Other logic and custom event handling ...
if (condition) {
onDragStart(el);
}
}
function onMove(e) {
// Other logic ...
if (wasDragging) {
onDrag(e);
}
}
function onMoveEnd(event) {
// Other logic ...
if (wasDragging) {
onDragEnd(event.target);
}
}
function onDragStart(el) {
draggingImage(el)
wasDragging = true;
manualSetData('helloMom', el)
}
function draggingImage(el) {
let image = draggable.cloneNode(true);
image.setAttribute("id", "drag-image");
image.style.width = el.offsetWidth + 'px'
image.style.height = el.offsetHeight + 'px'
image.style.position = 'fixed'
image.style.left = el.getBoundingClientRect().left + 'px';
image.style.top = el.getBoundingClientRect().top + 'px';
image.style.background = getComputedStyle(draggable).backgroundColor
image.style.opacity = .5
let div = document.body.appendChild(image);
}
function manualSetData(variable, v) {
data[variable] = v
}
function onDrag(e) {
let image = document.getElementById('drag-image')
image.style.left = image.getBoundingClientRect().left + e.movementX + 'px'
image.style.top = image.getBoundingClientRect().top + e.movementY + 'px'
let left = e.pageX;
let top = e.pageY;
let overlap = getDropzone(e, left, top)
drop.style.backgroundColor = allowedDrop ? 'green' : 'blue'
if (overlap) {
document.body.style.cursor = 'copy';
allowedDrop = true
} else {
document.body.style.cursor = 'no-drop';
allowedDrop = false
}
}
function getDropzone(e, left, top, id) {
// let rect1 = document.getElementById(id).getBoundingClientRect();
let rect1 = drop.getBoundingClientRect()
// to detect the overlap of mouse into the dropzone, as alternative of mouseover
var overlap = !(rect1.right < left ||
rect1.left > left ||
rect1.bottom < top ||
rect1.top > top)
return overlap
}
function onDragEnd(element) {
let image = document.getElementById('drag-image')
image.remove()
document.body.style.cursor = 'default';
if (allowedDrop) {
let getData = data['helloMom']
drop.appendChild(getData)
}
wasDragging = false;
}
#draggable {
background-color: red;
}
#drop {
background-color: blue;
}
<div id="draggable" draggable="true">
fancy content
</div>
<div id="drop">
drop area
</div>
I'm using React Three Fiber to animate a 3d model looking at the mouse. This is creating a canvas element in the background of my page. I have a few divs and h1s layered on top of it, and whenever my mouse hovers over the divs/h1s, the canvas freezes, until I mouse out of it. This results in a choppy looking animation. How do I rectify this?
This is my React Component:
function Model() {
const { camera, gl, mouse, intersect, viewport } = useThree();
const { nodes } = useLoader(GLTFLoader, '/scene.glb');
const canvasRef = useRef(null);
const group = useRef();
useFrame(({ mouse }) => {
const x = (mouse.x * viewport.width) / 1200;
const y = (mouse.y * viewport.height) / 2;
group.current.rotation.y = x + 3;
});
return (
<mesh
receiveShadow
castShadow
ref={group}
rotation={[1.8, 40, 0.1]}
geometry={nodes.HEAD.geometry}
material={nodes.HEAD.material}
dispose={null}></mesh>
);
}
You'll need to assign a special CSS rule to your <h1> and <div>s so that they don't capture pointer events:
h1 {pointer-events: none;}
This means the element is never the target of pointer events, so the mousemove event sort of "passes through" to the element below it. See the MDN docs for further description.
I have an AngularJS component that should react to either a single click or a drag (resizing an area).
I started to use RxJS (ReactiveX) in my application, so I try to find a solution using it. The Angular side of the request is minor...
To simplify the problem (and to train myself), I made a slider directive, based on the rx.angular.js drag'n'drop example: http://plnkr.co/edit/UqdyB2
See the Slide.js file (the other code is for other experiments). The code of this logic is:
function(scope, element, attributes)
{
var thumb = element.children(0);
var sliderPosition = element[0].getBoundingClientRect().left;
var sliderWidth = element[0].getBoundingClientRect().width;
var thumbPosition = thumb[0].getBoundingClientRect().left;
var thumbWidth = thumb[0].getBoundingClientRect().width;
// Based on drag'n'drop example of rx-angular.js
// Get the three major events
var mousedown = rx.Observable.fromEvent(thumb, 'mousedown');
var mousemove = rx.Observable.fromEvent(element, 'mousemove');
var mouseup = rx.Observable.fromEvent($document, 'mouseup');
// I would like to be able to detect a single click vs. click and drag.
// I would say if we get mouseup shortly after mousedown, it is a single click;
// mousedown.delay(200).takeUntil(mouseup)
// .subscribe(function() { console.log('Simple click'); }, undefined, function() { console.log('Simple click completed'); });
var locatedMouseDown = mousedown.map(function (event)
{
event.preventDefault();
// console.log('Click', event.clientX - sliderPosition);
// calculate offsets when mouse down
var initialThumbPosition = thumb[0].getBoundingClientRect().left - sliderPosition;
return { from: initialThumbPosition, offset: event.clientX - sliderPosition };
});
// Combine mouse down with mouse move until mouse up
var mousedrag = locatedMouseDown.flatMap(function (clickInfo)
{
return mousemove.map(function (event)
{
var move = event.clientX - sliderPosition - clickInfo.offset;
// console.log('Move', clickInfo);
// calculate offsets from mouse down to mouse moves
return clickInfo.from + move;
}).takeUntil(mouseup);
});
mousedrag
.map(function (position)
{
if (position < 0)
return 0;
if (position > sliderWidth - thumbWidth)
return sliderWidth - thumbWidth;
return position;
})
.subscribe(function (position)
{
// console.log('Drag', position);
// Update position
thumb.css({ left: position + 'px' });
});
}
That's mostly D'n'D constrained horizontally and to a given range.
Now, I would like to listen to mousedown, and if mouse up happens within a short while (say 200 ms, to adjust), I see it as a click and I do a specific treatment (eg. resetting the position to zero).
I tried with delay().takeUntil(mouseup), as seen in another SO answer, without success. Perhaps a switch() might be needed, too (to avoid going the drag route).
Any idea? Thanks in advance.
You can use timeout (timeoutWith if you are using ReactiveX/RxJS)
var click$ = mousedown.flatMap(function (md) {
return mouseup.timeoutWith(200, Observable.empty());
});
If the mouseup doesn't occur before the timeout it will just propagate an empty Observable instead. If it does then the downstream observer will receive an event.
Isn't the trick with delay(Xms).takeUntil(mouseup) doing the opposite of what you want? I mean, you want to detect when the mouseup event happens before the countdown, while the aformentioned trick detect when the mouseup event happens after.
I would try something around those lines (untested for now, but hopefully it will orient you in some positive direction):
var click$ = mousedown.flatMap(function ( mouseDownEv ) {
return merge(
Rx.just(mouseDownEv).delay(Xms).map(function ( x ) {return {event : 'noclick'};}),
mouseup.map(function ( mouseUpEv ) {return {event : mouseUpEv};})
).first();
});
The idea is to race the mouseup event against a dummy emission happening after your delay, and see who wins. So if click$ emits 'noclick' then you can consider that no click happened.
Hopefully that works, i will test soon but if you do before me, let me know.
Started using RxJs. Can't find a way around this problem. I have a draggable control:
startDrag = rx.Observable.fromEvent(myElem,'mousedown')
now, because the control is too small mousemove and mouseup events should be at document level (otherwise it won't stop dragging unless cursor exactly on the element)
endDrag = rx.Observable.fromEvent document,'mouseup'
position = startDrag.flatMap ->
rx.Observable.fromEvent document,'mousemove'
.map (x)-> x.clientX
.takeUntil endDrag
now how do I "catch" the right moment when is not being dragged anymore (mouseup).
you see the problem with subscribing to endDrag? It will fire every time clicked anywhere, and not just myElem
How do I check all 3 properties at once? It should take only those document.mouseups that happened exactly after startDrag and position
Upd: I mean the problem is not with moving the element. That part is easy - subscribe to position, change element's css.
My problem is - I need to detect the moment of mouseup and know the exact element that's been dragged (there are multiple elements on the page). How to do that I have no idea.
I have adapted the drag and drop example provided at the RxJS repo to behave as you need.
Notable changes:
mouseUp listens at document.
The targeted element is added to the return from select.
Drag movements are handled inside of map and map returns the element that was targeted in the mouseDown event.
Call last after takeUntil(mouseUp) so subscribe will only be reached when the drag process ends (once per drag).
Working example:
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 mousedrag = mousedown.selectMany(function(md) {
// calculate offsets when mouse down
var startX = md.offsetX;
var startY = md.offsetY;
// Calculate delta with mousemove until mouseup
return mousemove.select(function(mm) {
if (mm.preventDefault) mm.preventDefault();
else event.returnValue = false;
return {
// Include the targeted element
elem: mm.target,
pos: {
left: mm.clientX - startX,
top: mm.clientY - startY
}
};
})
.map(function(data) {
// Update position
dragTarget.style.top = data.pos.top + 'px';
dragTarget.style.left = data.pos.left + 'px';
// Just return the element
return data.elem;
})
.takeUntil(mouseup)
.last();
});
// Here we receive the element when the drag is finished
subscription = mousedrag.subscribe(function(elem) {
alert('Drag ended on #' + elem.id);
});
}
main();
#dragTarget {
position: absolute;
width: 20px;
height: 20px;
background: #0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.7/rx.all.min.js"></script>
<div id="dragTarget"></div>