Drag and Drop implemented using Rxjs not working - javascript

Trying to create a drag n drop implementation from an Rxjs course example, but its not working correctly. Some time the box is dragged back to original position some times it just get stuck. Here is the plunkr
https://plnkr.co/edit/9Nqx5qiLVwsOV7zU6Diw?p=preview
the js code:
var $drag = $('#drag');
var $document = $(document);
var $dropAreas = $('.drop-area');
var beginDrag$ = Rx.Observable.fromEvent($drag, 'mousedown');
var endDrag$ = Rx.Observable.fromEvent($document, 'mouseup');
var mouseMove$ = Rx.Observable.fromEvent($document, 'mousemove');
var currentOverArea$ = Rx.Observable.merge(
Rx.Observable.fromEvent($dropAreas, 'mouseover').map(e => $(e.target)),
Rx.Observable.fromEvent($dropAreas, 'mouseout').map(e => null)
);
var drops$ = beginDrag$
.do(e => {
e.preventDefault();
$drag.addClass('dragging');
})
.mergeMap(startEvent => {
return mouseMove$
.takeUntil(endDrag$)
.do(moveEvent => moveDrag(startEvent, moveEvent))
.last()
.withLatestFrom(currentOverArea$, (_, $area) => $area);
})
.do(() => {
$drag.removeClass('dragging')
.animate({top: 0, left: 0}, 250);
})
.subscribe( $dropArea => {
$dropAreas.removeClass('dropped');
if($dropArea) $dropArea.addClass('dropped');
});
function moveDrag(startEvent, moveEvent) {
$drag.css(
{left: moveEvent.clientX - startEvent.offsetX,
top: moveEvent.clientY - startEvent.offsetY}
);
}
If I remove the withLatestFrom operator, then dragging of div always work fine, but without this I cannot get the drop feature implemented.

Problem one: Some time the box is dragged back to original position some times it just get stuck.
Answer: you should replace order of chain, ".do" before ".withLatestFrom" like this:
const drops$ = beginDrag$
.do( e => {
e.preventDefault();
$drag.addClass('dragging');
})
.mergeMap(startEvent => {
return mouseMove$
.takeUntil(endDrag$)
.do(mouseEvent => {
moveDrag(startEvent, mouseEvent);
})
.last()
.do((x) => {
console.log("hey from last event",x);
$drag.removeClass('dragging')
.stop()
.animate({ top:0, left: 0}, 250);
}
)
.withLatestFrom(currentOverArea$, (_, $area) => {
console.log('area',$area);
return $area;
});
Problem two: drop and drag outside not working correctly.
Answer: because of mouse event causing by "pointer-events" is not clearly.
In Css File, at:
.dragable .dragging {
background: #555;
pointer-events: none;
}
This is not Enough, the "mouseout" (or "mouseleave") still working, so when you drag box and drop. it happening the same time event "mouseover" and "mouseout". So the drag area never change color.
What to do ?:
make it better by clear every mouse event from the target element. In this case, it is div#drag.dragable.dragging. Add only this to CSS and problem is solve.
div#drag.dragable.dragging {
pointer-events: none;
}
(Holly shit, it take me 8 hours to resolve this. Readmore or see Repo at: Repository
)

Related

ReactJS - Print specific elements in the DOM

I am using ReactJS on an App and currently need to be able to print some elements from the page on user's request (click on a button).
I chose to use the CSS media-query type print (#media print) to be able to check if an element should be printed, based on a selector that could be from a class or attribute on an Element. The strategy would be to hide everything but those "printable" elements with a stylesheet looking like:
#media print {
*:not([data-print]) {
display: none;
}
}
However, for this to work I need to also add the chosen print selector (here the attribute data-print) on every parent element each printable element has.
To do that here's what I've tried so far:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.setAttribute("data-print", "");
element = element.parentElement;
}
element.setAttribute("data-print", "");
}
});
};
const handleAfterPrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.removeAttribute("data-print");
element = element.parentElement;
}
element.removeAttribute("data-print");
}
});
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
With printNodeSelectors being a const Array of string selectors.
Unfortunately it seems that React ditch out all my dirty DOM modification right after I do them 😭
I'd like to find a way to achieve this without having to manually put everywhere in the app who should be printable, while working on a React App, would someone knows how to do that? 🙏🏼
Just CSS should be enough to hide all Elements which do not have the data-print attribute AND which do not have such Element in their descendants.
Use the :has CSS pseudo-class (in combination with :not one) to express that 2nd condition (selector on descendants):
#media print {
*:not([data-print]):not(:has([data-print])) {
display: none;
}
}
Caution: ancestors of Elements with data-print attribute would not match, hence their text nodes (not wrapped by a tag) would not be hidden when printing:
<div>
<span>should not print</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
Demo: https://jsfiddle.net/6x34ad50/1/ (you can launch the print preview browser feature to see the effect, or rely on the coloring)
Similar but just coloring to directly see the effect:
*:not([data-print]):not(:has([data-print])) {
color: red;
}
<div>
<span>should not print (be colored in red)</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
After some thoughts, tries and errors it appears that even though I managed to put the attribute selector on the parents I completely missed the children of the elements I wanted to print! (React wasn't at all removing the attributes from a mysterious render cycle in the end)
Here's a now functioning Component:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
const elements: Element[] = [];
// we need to give all parents and children a data-print attribute for them to be displayed on print
const addParents = (element: Element) => {
if (element.parentElement) {
elements.push(element.parentElement);
addParents(element.parentElement);
}
};
addParents(printableElement);
const addChildrens = (element: Element) => {
elements.push(element);
Array.from(element.children).forEach(addChildrens);
};
addChildrens(printableElement);
elements.forEach((element) => element.setAttribute("data-print", ""));
}
});
};
const handleAfterPrint = () => {
document.querySelectorAll("[data-print]").forEach((element) => element.removeAttribute("data-print"));
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
I usually don't like messing with the DOM while using React but here it allows me to keep everything in the component without having to modify anything else around (though I'd agree that those printNodeSelectors need to be chosen from outside and aren't dynamic at the moment)

js reverse remove, keep element to its DOM location

when we call element.remove(), element is removed from DOM since and element.isConnected will return false. is there a function that reverse remove method and put the element back into location?
let's say I have three elements: html_area, css_area and js_area. their display are managed individually by html_hider, js_hider, css_hider. when hider is clicked, I need to remove the area from DOM, also modifies container dimension. here's the thing, I want to keep these in order:
html
css
js
when they hide and show (appended or removed) form DOM, I want their sequence to be maintained. I manually coded it as:
html_area.connect = () => container.prepend(html_area);
css_area.connect = () => {
if(!html_area.isConnected){
container.prepend(css_area);
}else if(!js_area.isConnected){
container.append(css_area);
}else{
container.insertBefore(css_area, js_area);
}
}
js_area.connect = () => container.append(js_area);
html_hider.onclick = () => {
if(html_area.isConnected){
html_hider.classList.add('hide');
html_area.remove();
}else{
html_hider.classList.remove('hide');
html_area.connect();
}
aMode();
}
css_hider.onclick = () => {
if(css_area.isConnected){
css_hider.classList.add('hide');
css_area.remove();
}else{
css_hider.classList.remove('hide');
css_area.connect();
}
aMode();
}
js_hider.onclick = () => {
if(js_area.isConnected){
js_hider.classList.add('hide');
js_area.remove();
}else{
js_hider.classList.remove('hide');
js_area.connect();
}
aMode();
}
not robust and will be extremely complicated when elements and hiders pair more than 3. is there a way to reverse the remove method and put the element back in its original location?

Creating a tooltip on hover in React

I made a tooltip which appears when I hover on an element, and shows the full name of the product, productName.
<div
className="product-select-info"
onMouseEnter={e => productNameHandleHover(e)}
onMouseLeave={productNameHandleNoHover}
>
{productName}
<div
className="tooltip"
style={{
display: isTooltipShown ? "block" : "none",
top: mouseLocation.y,
left: mouseLocation.x,
}}
>
{productName}
</div>
</div>
And here are my handlers:
const productNameHandleHover = (event: any): void => {
setmouseLocation({
x: event.pageX,
y: event.pageY,
});
setisTooltipShown(true);
};
const productNameHandleNoHover = (): void => {
setisTooltipShown(false);
};
My problem is, I want to only show the tooltip after like 0.5 seconds. Currently, the tooltip appears as soon as the mouse goes over the div. How do I achieve this? I tried using setTimeout but I was just running into issues with that.
It is good to use css transitions as ritaj has mentioned in the comments.
But if you absolutely want javascript implementation, Whenever you are hovering over your element, set a class variable to be true.
const productNameHandleHover = (event: any): void => {
this.hovering = true;
...
}
and set it false whenever it is not.
const productNameHandleNoHover = (): void => {
this.hovering = false;
setisTooltipShown(false);
};
And when you actually set your tooltip check if your class variable is set or not.
const productNameHandleHover = (event: any): void => {
this.hovering = true;
setmouseLocation({
x: event.pageX,
y: event.pageY,
});
setTimeout(() => {
if (this.hovering) {
setisTooltipShown(true);
}
}, 500)
};
Here is a codesandbox that does what you need.
But you can already see the amount of effort you have to put in. So coming back to the original point. Using css transitions is a better option

How to say 'if ALL Instances of a CSS Class are Removed' in an if/else inside of a foreach Method

I'm having trouble removing the CSS 'active_bg' class of the wheel's core circle when it's removed from all segments.
The full code is on Github and Codepen.
Codepen: https://codepen.io/Rburrage/pen/xmqJoO
Github: https://github.com/RBurrage/wheel
In my click event, I tried saying that if the class exists on a segment, add it to the core circle, too -- ELSE -- remove it from the core circle. My code is within a forEach method that loops through all groups in the SVG.
The part in question is in the last event listener below (the 'click' event).
var secondGroups = document.querySelectorAll('.sols-and-mods');
secondGroups.forEach(function (secondGroup) {
let solution = secondGroup.childNodes[1];
let module = secondGroup.childNodes[3];
let core = document.querySelector('.core_background');
secondGroup.addEventListener('mouseover', () => {
solution.classList.add('hovered_bg');
module.classList.add('hovered_bg');
})
secondGroup.addEventListener('mouseout', () => {
solution.classList.remove('hovered_bg');
module.classList.remove('hovered_bg');
})
secondGroup.addEventListener('click', () => {
solution.classList.toggle('active_bg');
module.classList.toggle('active_bg');
if (solution.classList.contains('active_bg')) {
core.classList.add('active_bg');
solution.classList.remove('hovered_bg');
module.classList.remove('hovered_bg');
}else{
core.classList.remove('active_bg');
}
})
})
When the user clicks on a segment of the wheel, the CSS 'active_bg' class gets added to both the clicked segment and the wheel's core circle.
I want to remove the 'active_bg' class from the wheel's core circle but only when it is removed from ALL segments.
Currently, as soon as I remove the class from any ONE segment, it gets removed from the core circle.
Can someone please tell me what I'm doing wrong?
Thank you!
Explained
Okay, so to keep this as short and simply as possible, I changed your logic only ever so slightly, I've just included a check to see if there's at least one option selected, if not, then the core circle has the default class, otherwise, it will continue to have the class name of active_bg.
Here's the JSFiddle that I've made.
If there's any further issues with this solution, don't hesitate to ask.
Edit
I just thought I'd go ahead an include the JavaScript that I was playing around with.
window.onload = function() {
TweenMax.staggerFrom('.solution', .5, {
opacity: 0,
delay: 0.25
}, 0.1);
TweenMax.staggerFrom('.module', .5, {
opacity: 0,
delay: 0.5
}, 0.1);
}
var secondGroups = document.querySelectorAll('.sols-and-mods');
secondGroups.forEach(function(secondGroup) {
let solution = secondGroup.childNodes[1];
let module = secondGroup.childNodes[3];
let core = document.querySelector('.core_background');
secondGroup.addEventListener('mouseover', () => {
solution.classList.add('hovered_bg');
module.classList.add('hovered_bg');
})
secondGroup.addEventListener('mouseout', () => {
solution.classList.remove('hovered_bg');
module.classList.remove('hovered_bg');
})
secondGroup.addEventListener('click', () => {
solution.classList.toggle('active_bg');
module.classList.toggle('active_bg');
if (solution.classList.contains('active_bg')) {
core.classList.add('active_bg');
solution.classList.remove('hovered_bg');
module.classList.remove('hovered_bg');
}
// Added this line.
if (document.querySelector(".sols-and-mods .active_bg") == null) {
core.classList.remove('active_bg');
}
})
})

Detect click outside div using javascript

I'd like to detect a click inside or outside a div area. The tricky part is that the div will contain other elements and if one of the elements inside the div is clicked, it should be considered a click inside, the same way if an element from outside the div is clicked, it should be considered an outside click.
I've been researching a lot but all I could find were examples in jquery and I need pure javascript.
Any suggestion will be appreciated.
It depends on the individual use case but it sounds like in this example there are likely to be other nested elements inside the main div e.g. more divs, lists etc. Using Node.contains would be a useful way to check whether the target element is within the div that is being checked.
window.addEventListener('click', function(e){
if (document.getElementById('clickbox').contains(e.target)){
// Clicked in box
} else{
// Clicked outside the box
}
});
An example that has a nested list inside is here.
You can check if the clicked Element is the div you want to check or not:
document.getElementById('outer-container').onclick = function(e) {
if(e.target != document.getElementById('content-area')) {
console.log('You clicked outside');
} else {
console.log('You clicked inside');
}
}
Referring to Here.
you can apply if check for that inside your click event
if(event.target.parentElement.id == 'yourID')
In Angular 6 and IONIC 3, I do same as here:
import {Component} from 'angular/core';
#Component({
selector: 'my-app',
template: `
<ion-content padding (click)="onClick($event)">
<div id="warning-container">
</div>
</ion-content>
`
})
export class AppComponent {
onClick(event) {
var target = event.target || event.srcElement || event.currentTarget;
if (document.getElementById('warning-container').contains(target)){
// Clicked in box
} else{
// Clicked outside the box
}
}
}
This working fine on web/android/ios.
It might be helpful for someone, Thanks.
Try this solution it uses pure javascript and it solves your problem. I added css just for better overview... but it is not needed.
document.getElementById('outer-div').addEventListener('click', function(){
alert('clicked outer div...');
});
document.getElementById('inner-div').addEventListener('click', function(e){
e.stopPropagation()
alert('clicked inner div...');
});
#outer-div{
width: 200px;
height: 200px;
position: relative;
background: black;
}
#inner-div{
top: 50px;
left: 50px;
width: 100px;
height: 100px;
position: absolute;
background: red;
}
<div id="outer-div">
<div id="inner-div">
</div>
</div>
I came up with a hack for this that's working well for me and that might help others.
When I pop up my dialog DIV, I simultaneously display another transparent DIV just behind it, covering the whole screen.
This invisible background DIV closes the dialog DIV onClick.
This is pretty straightforward, so I'm not going to bother with the code here. LMK in the comments if you want to see it and I'll add it in.
HTH!
closePopover () {
var windowBody = window
var popover = document.getElementById('popover-wrapper') as HTMLDivElement;
windowBody?.addEventListener('click', function(event){
if(popover === event.target) {
console.log("clicked on the div")
}
if(popover !== event.target) {
console.log("clicked outside the div")
}
})
}
}
I recently needed a simple vanilla JS solution which solves for:
Ignoring specific selectors including whether a parent contains one of these selectors
Ignoring specific DOM nodes
This solution has worked quite well in my app.
const isClickedOutsideElement = ({ clickEvent, elToCheckOutside, ignoreElems = [], ignoreSelectors = [] }) => {
const clickedEl = clickEvent.srcElement;
const didClickOnIgnoredEl = ignoreElems.filter(el => el).some(element => element.contains(clickedEl) || element.isEqualNode(clickedEl));
const didClickOnIgnoredSelector = ignoreSelectors.length ? ignoreSelectors.map(selector => clickedEl.closest(selector)).reduce((curr, accumulator) => curr && accumulator, true) : false;
if (
isDOMElement(elToCheckOutside) &&
!elToCheckOutside.contains(clickedEl) &&
!didClickOnIgnoredEl &&
!didClickOnIgnoredSelector
){
return true;
}
return false;
}
const isDOMElement = (element) => {
return element instanceof Element || element instanceof HTMLDocument;
}
In React you can use useClickOutside hook from react-cool-onclickoutside.
Demo from Github:
import { useClickOutside } from 'use-events';
const Example = () => {
const ref1 = React.useRef(null);
const ref2 = React.useRef(null);
const [isActive] = useClickOutside([ref1, ref2], event => console.log(event));
return (
<div>
<div ref={ref1} style={{ border: '1px dotted black' }}>
You are {isActive ? 'clicking' : 'not clicking'} outside of this div
</div>
<br />
<div ref={ref2} style={{ border: '1px dotted black' }}>
You are {isActive ? 'clicking' : 'not clicking'} outside of this div
</div>
</div>
);
};
Live demo

Categories