Drag and drop with touch support for react.js - drag-and-drop

How to implement drag and drop for Facebooks' react.js with support for touch events?
There's a couple of questions and articles and libraries about drag and drop for react.js, but none of them seems to mention touch events, and none of the demo's work on my phone.
In general I wonder what would be the easiest: Try to implement this using existing d&d libraries which already support touch, but may need some work to properly work together with react. Or try to use any of the react d&d examples and make them work with touch (which, seeing this issue, may not be trivial?)

react-motion (with touch events)
We have tried "react-motion" for dragging items in a list. With more than 15-20 items it becomes really laggy. (But with small list it works good, like in this demo). Be aware that mobile devices are much slower than desktops.
Important note about react-motion: Don't forget to use production mode when testing your animation's performance!
react-dnd (with touch events)
The second option was "react-dnd". It is a great library. It is low level, however, it is rather easy to understand how to work with it. But at first, "react-dnd" was not an option for us because of no touch events support.
Later, when Yahoo had released react-dnd-touch-backend we decided to switch our app from "react-motion" to "react-dnd". This solved all our performance issues. We have list 50-70 items and it just works as expected.
Yahoo has done really good work and the solution works in our production apps.

You already mentioned react-dnd and I make PR that enable dnd for touch devices so you can try it

I haven't found any answer to this yet. The accepted answer is not really an answer but it points to a github library. I am going to try to include here a complete answer using only react.
Here it goes, the code should be self explanatory, but a couple of words ahead of time. We need to use a lot of state variables to keep state between renders, otherwise any variables get reset out. To make the transitions smooth, I update the position once a render was completed using useEffect hook. I tested this in codesandbox, I'm including the link here for anybody to edit the code and play with it, just fork it. It workd with the MS Surface Book2 Pro
and Android. It has a formatting problem with the iPhone IOS. Both for Safari and Chrome. If somebody fixes it that'd be great. For now I have what I need and claim success.
Here are the files under src in codesandbox.io:
App.js
import "./styles/index.pcss";
import "./styles/tailwind-pre-build.css";
import Photos from "./Photos.js";
export default function App() {
return (
<>
<div className="flow-root bg-green-200">
<div className="my-4 bg-blue-100 mb-20">
Drag and Drop with touch screens
</div>
</div>
<div className="flow-root bg-red-200">
<div className="bg-blue-100">
<Photos />
</div>
</div>
</>
);
}
Photos.js:
import React, { useState } from "react";
import "./styles/index.pcss";
import Image from "./image";
export default function Photos() {
const [styleForNumber, setStyleForNumber] = useState({
position: "relative",
width: "58px",
height: "58px"
});
const photosArray = [
"https://spinelli.io/noderestshop/uploads/G.1natalie.1642116451444",
"https://spinelli.io/noderestshop/uploads/G.2natalie.1642116452437",
"https://spinelli.io/noderestshop/uploads/G.3natalie.1642116453418",
"https://spinelli.io/noderestshop/uploads/G.4natalie.1642116454396",
"https://spinelli.io/noderestshop/uploads/G.5natalie.1642116455384",
"https://spinelli.io/noderestshop/uploads/G.6natalie.1642116456410",
"https://spinelli.io/noderestshop/uploads/G.7natalie.1642116457466",
"https://spinelli.io/noderestshop/uploads/G.8natalie.1642116458535",
"https://spinelli.io/noderestshop/uploads/G.0natalie.1642116228246"
];
return (
<>
<div
className="w-1/2 bg-green-200"
style={{
display: "grid",
gridTemplateColumns: "[first] 60px [second] 60px [third] 60px",
gridTemplateRows: "60px 60px 60px",
rowGap: "10px",
columnGap: "20px",
position: "relative",
justifyContent: "center",
placeItems: "center"
}}
>
{photosArray.map((photo, i) => (
<div
className="relative z-1 h-full w-full flex flex-wrap content-center touch-none"
key={i}
>
<div className="contents">
<Image photo={photo} i={i} />
</div>
</div>
))}
</div>
</>
);
}
Image.js:
import React, { useRef, useState, useEffect } from "react";
import "./styles/index.pcss";
export default function Image({ photo, i }) {
const imgRef = useRef();
const [top, setTop] = useState(0);
const [left, setLeft] = useState(0);
const [drag, setDrag] = useState(false);
const [styleForImg, setStyleForImg] = useState({
position: "absolute",
width: "58px",
height: "58px"
});
const [offsetTop, setOffsetTop] = useState(-40);
const [offsetLeft, setOffsetLeft] = useState(0);
const [xAtTouchPointStart, setXAtTouchPointStart] = useState(0);
const [yAtTouchPointStart, setYAtTouchPointStart] = useState(0);
useEffect(() => {
if (drag) {
setStyleForImg({
position: "relative",
width: "58px",
height: "58px",
top: top,
left: left
});
} else {
setStyleForImg({
position: "relative",
width: "58px",
height: "58px"
});
}
console.log("style: ", styleForImg);
}, [drag, top, left]);
const handleTouchStart = (e, i) => {
e.preventDefault();
let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0];
const x = +touch.pageX;
const y = +touch.pageY;
console.log(
"onTouchStart coordinates of icon # start: X: " + x + " | Y: " + y
);
console.log("dragged from position n = ", i + 1);
// get the mouse cursor position at startup:
setXAtTouchPointStart(x);
setYAtTouchPointStart(y);
setDrag(true);
};
const handleTouchEnd = (e) => {
// if (process.env.NODE_ENV === 'debug5' || process.env.NODE_ENV === 'development') {
e.preventDefault();
setDrag(false);
console.log(
new Date(),
"onTouchEnd event, coordinates of icon # end: X: " +
e.changedTouches[0]?.clientX +
" | Y: " +
e.changedTouches[0]?.clientY +
" | top: " +
top +
" | left: " +
left
);
};
const handleElementDrag = (e) => {
e = e || window.event;
e.preventDefault();
let x = 0;
let y = 0;
//Get touch or click position
//https://stackoverflow.com/a/41993300/5078983
if (
e.type === "touchstart" ||
e.type === "touchmove" ||
e.type === "touchend" ||
e.type === "touchcancel"
) {
let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0];
x = +touch.pageX; // X Coordinate relative to the viewport of the touch point
y = +touch.pageY; // same for Y
} else if (
e.type === "mousedown" ||
e.type === "mouseup" ||
e.type === "mousemove" ||
e.type === "mouseover" ||
e.type === "mouseout" ||
e.type === "mouseenter" ||
e.type === "mouseleave"
) {
x = +e.clientX;
y = +e.clientY;
}
console.log("x: ", x, "y: ", y);
// calculate the new cursor position:
const xRelativeToStart = x - xAtTouchPointStart;
console.log(
"xRel = ",
x,
" - ",
xAtTouchPointStart,
" = ",
xRelativeToStart
);
const yRelativeToStart = y - yAtTouchPointStart;
console.log(
"yRel = ",
y,
" - ",
yAtTouchPointStart,
" = ",
yRelativeToStart
);
// setXAtTouchPointStart(x); // Reseting relative point to current touch point
// setYAtTouchPointStart(y);
// set the element's new position:
setTop(yRelativeToStart + "px");
setLeft(xRelativeToStart + "px");
console.log("top: ", yRelativeToStart + "px");
console.log("Left: ", xRelativeToStart + "px");
};
const handleDragEnd = (e) => {
// if (process.env.NODE_ENV === 'debug5' || process.env.NODE_ENV === 'development') {
console.log(
new Date(),
"Coordinates of icon # end X: " + e.clientX + " | Y: " + e.clientY
);
};
const handleDragStart = (e, i) => {
// From https://stackoverflow.com/a/69109382/15355839
e.stopPropagation(); // let child take the drag
e.dataTransfer.dropEffect = "move";
e.dataTransfer.effectAllowed = "move";
console.log(
"Coordinates of icon # start: X: " + e.clientX + " | Y: " + e.clientY
);
// console.log ('event: ', e)
console.log("dragged from position n = ", i + 1);
};
return (
<img
ref={imgRef}
className="hover:border-none border-4 border-solid border-green-600 mb-4"
src={photo}
alt="placeholder"
style={styleForImg}
onDragStart={(e) => handleDragStart(e, i)}
onDragEnd={handleDragEnd}
onTouchStart={(e) => handleTouchStart(e, i)}
onTouchEnd={handleTouchEnd}
onTouchMove={handleElementDrag}
></img>
);
}
index.js:
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import "./styles/index.pcss";
import App from "./App";
const root = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
root
);
styles.css:
.Main {
font-family: sans-serif;
text-align: center;
}
/styles/index.pcss:
#tailwind base;
#tailwind components;
#tailwind utilities;
I couldn't make tailwinds grid work, so I used the actual css inline styles. No idea why they didn't in codesandbox.

Related

How can I disable or change the href on a React-Leaflet v4 Popup close button?

In react-leaflet v4, the Popup component has a default href associated with the close button that directs to #close. Is there a way in React to modify this href or disable the href redirection? It's breaking my react-dom HashRouter.
Of note, Popup.js in Leaflet 1.8 has the following code:
var closeButton = this._closeButton = DomUtil.create('a', prefix + '-close-button', container);
closeButton.setAttribute('role', 'button'); // overrides the implicit role=link of <a> elements #7399
closeButton.setAttribute('aria-label', 'Close popup');
closeButton.href = '#close';
closeButton.innerHTML = '<span aria-hidden="true">×</span>';
The same issue is also in angular - means it seems to be the leaflet Lib:
// leaflet.js
close: function () {
if (this._map) {
this._map.removeLayer(this);
}
return this;
},
The close function has not even the $event as an argument and the "default" isn't prevented. This leaves us only dirty hacks:
Get the close button after the marker was displayed
Add a click handler more
Add a prefentDefault
yourMethodOpensTheToolTip(marker: Marker) {
if (marker && marker.openPopup) {
marker.openPopup();
// 1. get the close buttons, after the opened the popup
const closeButtons = document.getElementsByClassName('leaflet-popup-close-button');
// 2. add the event handler - if you have more than one, loop here
if (closeButtons && closeButtons.length > 0) {
L.DomEvent.on(closeButtons[0] as HTMLElement, 'click', function(ev){
ev.preventDefault(); // 3. stop it here
});
}
Just for reference the #close button as HTML:
Try something like this. It will probably disable any other hrefs that you may have in the popup though.
document.querySelector('.leaflet-pane.leaflet-popup-pane')!.addEventListener('click', event => {
event.preventDefault();
});
You can utilize useRef hooks and create a click event in the marker
const mapRef = useRef(null);
// event listener to handle marker click
const handleClick = () => {
mapRef.current._popup._closeButton.addEventListener('click', (event) => {
event.preventDefault();
})
};
const map = (<MapContainer center={position} zoom={13} scrollWheelZoom={false} style={{ height: '350px', width: '100%' }} ref={mapRef}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker
position={position}
eventHandlers={{
click: (e) => handleClick(),
}}
>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
</Marker>
</MapContainer>)
if you are using GeoJSON you can use onEachFeature props
const onEachCountry = (country, layer) => {
const countryName = country.properties.ADMIN;
layer.on('click', function (e) {
layer
.bindPopup(countryName)
.openPopup()
._popup._closeButton.addEventListener('click', (event) => {
event.preventDefault();
});
});
};
const map = (<MapContainer style={{ height: '300px' }} zoom={1} center={[20, 100]}>
<GeoJSON style={countryStyle} data={mapData.features} onEachFeature={onEachCountry} />
</MapContainer>)
In my React project with react-leaflet v4, I had the same issue and I solved it with the "popupopen" event :
https://leafletjs.com/reference.html#marker-popupopen
<Marker
position={position}
eventHandlers={{
popupopen: (e) => {
e.popup._closeButton.removeAttribute("href");
e.popup._closeButton.style.cursor = "pointer";
}
}}
>
<Popup>
<p>Lorem ipsum dolor sit amet</p>
</Popup>
</Marker>
I hope it will help.
Building on Paul's answer. Here is the solution if you have multiple popups. This will handle the close button click event on all the popups that are open on the leaflet map.
// This is a stopgap till Leaflet fixes its issue with close buttons in popups in Leaflet maps
let popupCloseButtonsHTMLCollection = document.getElementsByClassName('leaflet-popup-close-button');
if(popupCloseButtonsHTMLCollection && popupCloseButtonsHTMLCollection.length > 0){
//convert the popupCloseButtonsHTMLCollection to array
var popupArray = [].slice.call(popupCloseButtonsHTMLCollection);
popupArray.forEach(button =>{
L.DomEvent.on(button as HTMLElement, 'click', function(ev){
ev.preventDefault();
});
});
}

How can I change the behaviour of bindPopup

Good day everybody.
I bought a nice template and leaftlet is used to show maker.
Here is the demo . Actually when you clik on a marker, it open a widnows with a picture and some température value.
I would like to have all of the windows open. Of course, I am going to modify the html, to remove the picture and some information as GPS, and only keep the temperatue value. The goal is to be able to immediately see the temperature boxes below the markers. Optionaly, when I click on the marker it redirect to another page, same you click on the picture.
My first problem, I can not find the jacasvript script which work with the link of marker. The idea would be to cancel the effect of the click, or as I wrote, after we click it open the graph page instead of opening the windows.
My first question: how can I find a do to change the action of the click, on the marker
My second question (may be it be cancel the 1st question :) ), how can I change the behaviour of the bindpopup? Is there way "to tell" to the bindpopup, stay always open?
My thirst question: Or can we add one or two additional nice boxes, which show always the temperature below the marker, and keep the bindPopup, as it is? That would be nice as well
Here is the code of the map line 215
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// OpenStreetMap - Homepage
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function createHomepageOSM(_latitude,_longitude,_nbField){
setMapHeight();
if( document.getElementById('map') != null ){
var map = L.map('map', {
center: [_latitude,_longitude],
zoom: 18,
scrollWheelZoom: false
});
//L.tileLayer('http://openmapsurfer.uni-hd.de/tiles/roadsg/x={x}&y={y}&z={z}', {
L.tileLayer('http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
//subdomains: '0123',
maxZoom: 20,
attribution: 'OpenStreetMap contributors, CC-BY-SA'
}).addTo(map);
var markers = L.markerClusterGroup({
showCoverageOnHover: false
});
function locateUser() {
$('#map').addClass('fade-map');
map.locate({setView : true})
}
$('.geo-location').on("click", function() {
locateUser();
});
$.ajax({
type: "POST",
url: "sql/get_map.mysql.php",
//data:'node=node1',
//data:{node_id:"firstnode", node2:"secondnode", node3:"thirdnode", from:"from", to:"to"}, // Send parameter to get.php
success: result,
error: error,
dataType: "json"
});
function error(data)
{
$('body').addClass('loaded');
alert("Error getting datas from DB");
console.log("Error getting datas from DB");
console.log(data);
}
function result(data){
console.info("data:",data);
var allMarkers=[];
var nhtml = '<img src="assets/img/property-types/vineyard.png">';
for (var i = 0; i < data.properties.length; i++) {
allMarkers.push(L.latLng(data.properties[i]['la'], data.properties[i]['lo']));
//data.properties[i]['b2'] = 0;
if((data.properties[i]['b1']>=data.properties[i]['se'] && data.properties[i]['b1'] < data.properties[i]['se']+1) ||
(data.properties[i]['b2']>=data.properties[i]['se'] && data.properties[i]['b2'] < data.properties[i]['se']+1) ||
(data.properties[i]['b3']>=data.properties[i]['se'] && data.properties[i]['b3'] < data.properties[i]['se']+1) ||
(data.properties[i]['b4']>=data.properties[i]['se'] && data.properties[i]['b4'] < data.properties[i]['se']+1)
)
{
nhtml = '<img src="assets/img/property-types/vineyard-orange.png">';
}
if(((data.properties[i]['b1'] < data.properties[i]['se']) && data.properties[i]['b1'] != null) ||
((data.properties[i]['b2'] < data.properties[i]['se']) && data.properties[i]['b2'] != null) ||
((data.properties[i]['b3'] < data.properties[i]['se']) && data.properties[i]['b3'] != null) ||
((data.properties[i]['b4'] < data.properties[i]['se']) && data.properties[i]['b4'] != null)
)
{
nhtml = '<img src="assets/img/property-types/vineyard-red.png">';
}
else{
nhtml = '<img src="assets/img/property-types/vineyard.png">';
}
var _icon = L.divIcon({
//html: '<img src="' + locations[i][7] +'">',
html: nhtml,
iconSize: [40, 48],
iconAnchor: [20, 48],
popupAnchor: [0, -48]
});
var title = data.properties[i]['station'];
var marker = L.marker(new L.LatLng(data.properties[i]['la'],data.properties[i]['lo']), {
title: title,
icon: _icon
});
var str ='';
if(data.properties[i]['b1'] != null)
{
str = str.concat('<div class="tag price"> ' + data.properties[i]['b1'] + '°C</div>');
}
if(data.properties[i]['b2'] != null)
{
str = str.concat('<div class="tag price"> ' + data.properties[i]['b2'] + '°C</div>');
}
if(data.properties[i]['b3'] != null)
{
str = str.concat('<div class="tag price"> ' + data.properties[i]['b3'] + '°C</div>');
}
if(data.properties[i]['b4'] != null)
{
str = str.concat('<div class="tag price"> ' + data.properties[i]['b4'] + '°C</div>');
}
marker.bindPopup(
'<div class="property">' +
'<a data-field=' + data.properties[i]['id_field'] +'" data-station=' + data.properties[i]['id_station'] +'" href="charts.php?field='+ data.properties[i]['id_field'] +'">' +
'<div class="property-image">' +
'<img src="img/stations/station-' + data.properties[i]['id_station'] + '.jpg">' +
'</div>' +
'<div class="overlay">' +
'<div class="info">' +
'<h3>' + data.properties[i]['station'] + '</h3>' +
'<figure>' + data.properties[i]['da'] + '</figure>' +
'<figure>' + data.properties[i]['la'] + ' ' + data.properties[i]['lo'] +'</figure>' +
str +
'<div class="tag"> ' + data.properties[i]['se'] + '°C</div>' +
'</div>' +
'</div>' +
'</a>' +
'</div>'
);
markers.addLayer(marker);
}
if(_nbField>1){
bounds = L.latLngBounds(allMarkers);
map.fitBounds(bounds,{ padding: [10, 10] });
}
map.addLayer(markers);
map.on('locationfound', onLocationFound);
function onLocationFound(){
$('#map').removeClass('fade-map');
}
$('body').addClass('loaded');
setTimeout(function() {
$('body').removeClass('has-fullscreen-map');
}, 1000);
$('#map').removeClass('fade-map');
}
}
}
My last question, with firefox, id possible "to track" the javascript action?
Feel free to aks complementary question to better understand and help, if I missed to provide information.
Many thanks
You can add a click event to the marker:
marker.on('click',(e)=>{
console.log(e);
});
Show all Popups:
You need to set the options autoClose and closeOnClick to false:
marker.bindPopup(content,{autoClose: false, closeOnClick: false});
You can use Tooltips:
marker.bindTooltip('5.3°C', {direction: 'bottom', permanent: true});
I don't know exactly what do you mean, but it sounds like debugging. Use the developer console with the Debugger.
Thanks for your reply and help
Unfortunately 2. does not work. There is no differences. I added closeButton:true, and that works, but it's not what I need.
marker.bindPopup(
'<div class="property">' +
'<a data-field=' + data.properties[i]['id_field'] +'" data-station=' + data.properties[i]['id_station'] +'" href="charts.php?field='+ data.properties[i]['id_field'] +'#st-'+ data.properties[i]['id_station'] +'">' +
'<div class="property-image">' +
'<img src="img/stations/station-' + data.properties[i]['id_station'] + '.jpg">' +
'</div>' +
'<div class="overlay">' +
'<div class="info">' +
'<h3>' + data.properties[i]['station'] + '</h3>' +
'<figure>' + data.properties[i]['da'] + '</figure>' +
'<figure>' + data.properties[i]['la'] + ' ' + data.properties[i]['lo'] +'</figure>' +
str +
'<div class="tag"> ' + data.properties[i]['se'] + '°C</div>' +
'</div>' +
'</div>' +
'</a>' +
'</div>',{autoClose: true, closeOnClick: false, closeButton: true}
);
I also tried the interesting option with tooltip. Bellow the above code I added
marker.bindTooltip('5.3°C', {direction: 'bottom', permanent: true});
But that print an error message
marker.bindTooltip is not a function
Is there additionally library I have to add for tooltop, or is inlcuded into leafet.
(bindTootip would be great and enough for my need)
Thanks for helping
Cheers

Performance issues with 1k+ markers with popups in React Leaflet

I have a React application with the React Leaflet library and I'm displaying a marker for each building in the map, in a small town. I have about 5k markers in total and a filter to display only the markers I want.
However, I noticed that I was having a huge performance hit with the code below. I've looked at some alternatives such as PixiOverlay and marker clustering, but the former is quite complicated to migrate the current code base to and the latter doesn't solve my problems at all.
My current code:
import React, {
useRef, useEffect, useContext, useState,
} from 'react';
import ReactDOMServer from 'react-dom/server';
import {
Marker, useLeaflet, Popup, Tooltip, CircleMarker, Circle,
} from 'react-leaflet';
import L from 'leaflet';
import styled from 'styled-components';
interface IProps {
coords: [number, number]
description: string,
name: string
}
let timeoutPopupRef: any = null;
let timeoutPopupRefClose: any = null;
const DynamicMarker: React.FC<IProps> = ({ coords, description, name }) => {
const markerRef = useRef<any>(null);
const popupRef = useRef<Popup>(null);
const tooltipRef = useRef<Tooltip>(null);
const leaflet = useLeaflet();
const divIcon: L.DivIcon = L.divIcon({
iconSize: [25, 25],
className: 'marker-white',
});
const onComponentMount = () => {
if (!leaflet.map) return;
if (!markerRef.current) return;
const mapZoom: number = leaflet.map.getZoom();
if (popupRef.current) {
if (mapZoom <= 17) {
markerRef.current.leafletElement.unbindPopup();
} else if (mapZoom > 17) {
markerRef.current.leafletElement.bindPopup(popupRef.current!.leafletElement);
}
}
if (tooltipRef.current) {
if (mapZoom <= 15) {
markerRef.current.leafletElement.unbindTooltip();
} else if (mapZoom > 15) {
markerRef.current.leafletElement.bindTooltip(tooltipRef.current!.leafletElement);
}
}
leaflet.map!.on('zoomend', onMapZoomEnd);
};
useEffect(onComponentMount, []);
const onMapZoomEnd = () => {
if (!markerRef.current) return;
if (!popupRef.current) return;
if (!leaflet.map) return;
const zoom = leaflet.map.getZoom();
if (zoom < 17) {
if (!markerRef.current!.leafletElement.isPopupOpen()) {
markerRef.current!.leafletElement.unbindPopup();
}
} else if (zoom >= 17) {
markerRef.current!.leafletElement.bindPopup(popupRef.current.leafletElement);
}
};
const handlePopupVisible = (value: boolean) => {
if (!markerRef.current) return;
if (timeoutPopupRefClose) clearTimeout(timeoutPopupRefClose);
if (value) {
if (!markerRef.current!.leafletElement.isPopupOpen()) {
timeoutPopupRef = setTimeout(() => {
markerRef.current!.leafletElement.openPopup();
}, 400);
}
} else {
if (timeoutPopupRef) {
clearTimeout(timeoutPopupRef);
}
if (markerRef.current!.leafletElement.isPopupOpen()) {
timeoutPopupRefClose = setTimeout(() => {
markerRef.current!.leafletElement.closePopup();
}, 100);
}
}
};
const onComponentDismount = () => {
leaflet.map!.off('zoomend', onMapZoomEnd);
if (!markerRef.current) return;
markerRef.current.leafletElement.remove();
};
useEffect(() => onComponentDismount, []);
return (
<Marker
icon={divIcon}
position={coords}
onmouseover={() => handlePopupVisible(true)}
onmouseout={() => handlePopupVisible(false)}
ref={markerRef}
>
<Popup className="custom-popup-content" ref={popupRef} closeButton={false}>
<div
onMouseEnter={() => handlePopupVisible(true)}
onMouseLeave={() => handlePopupVisible(false)}
>
<img
className="popup-img"
alt='image'
src='https://cdn.discordapp.com/attachments/578931223775281162/644181902215086094/default_geocode-1x.png'
/>
<div className="popup-content">
<span className="popup-content-title">{name}</span>
{description && <span className="popup-content-subtitle">{description}</span>}
</div>
</div>
</Popup>
</Marker>
);
};
export default DynamicMarker;
The code above unbinds popups from markers if the map zoom is below a threshold, and binds them when the zoom is above the threshold. I also implemented event handlers to onMouseOver and onMouseOut events on the marker component to open my popup when the user hovers the marker icon and it will only close the popup if the cursor isn't hovering over the popup or the marker icon.
When I zoom in or out with about 2k markers being displayed, the map freezes for about 5-10 seconds and updates all of the components inside the Map component exported by react-leaflet.
After testing with marker clustering via react-leaflet-markercluster, I noticed that the performance issues were still present. I tried commenting out the Popup component passed as a children to the marker component and the lag issues I had were gone.
With that in mind, I realized that my bottleneck was actually rendering 2k popups in the DOM even though they were invisible. So, after some trial and error, I came across a solution: states.
I added a boolean state called shouldDrawPopup, with a default value of false and only changed its value inside the handlePopupVisible function. The value of this boolean state will change only if:
Map zoom is above a threshold; and
Popup is not open
And then I changed the render function of my component to include a popup only if the shouldDrawPopup state is true:
return (
{shouldDrawPopup && (
<Marker
icon={divIcon}
position={coords}
onmouseover={() => handlePopupVisible(true)}
onmouseout={() => handlePopupVisible(false)}
ref={markerRef}
>
<Popup className="custom-popup-content" ref={popupRef} closeButton={false}>
<div
onMouseEnter={() => handlePopupVisible(true)}
onMouseLeave={() => handlePopupVisible(false)}
>
<img
className="popup-img"
alt='image'
src='https://cdn.discordapp.com/attachments/578931223775281162/644181902215086094/default_geocode-1x.png'
/>
<div className="popup-content">
<span className="popup-content-title">{name}</span>
{description && <span className="popup-content-subtitle">{description}</span>}
</div>
</div>
</Popup>
</Marker>
)}
);
If anyone has other solutions or any feedback to this problem, feel free to share!

How to make anchor move with respect to a block dragged in jsplumb?

I am trying to build a flowchart using drag and drop options. The user should be able to drag an element from one div and drop it to another.
Now I'm able to drag and drop. I have given an option such that on dropping the block, anchors should appear on them. And I'm able to link these blocks with connectors using js plumb.
I have given the draggable option for dropped blocks. The problem is whenever I drag connected blocks, the anchors' position does not change.
How to make a change such that whenever I drag any block, its anchor and connecting lines should also drag?
Here's my code:
jsPlumb.ready(function() {
var EndpointOptions = {
setDragAllowedWhenFull: true,
endpoint: "Dot",
maxConnections: 10,
paintStyle: {
width: 21,
height: 21,
fillStyle: '#666',
},
isSource: true,
connectorStyle: {
strokeStyle: "#666"
},
isTarget: true,
dropOptions: {
drop: function(e, ui) {
alert('drop!');
}
}
};
var count = 0;
var x = "";
//Make element draggable
$(".drag").draggable({
helper: 'clone',
cursor: 'move',
tolerance: 'fit',
revert: true
});
$(".droppable").droppable({
accept: '.drag',
activeClass: "drop-area",
<!-- stop: function( event, ui ) {}, -->
drop: function(e, ui) {
if ($(ui.draggable)[0].id !== "") {
x = ui.helper.clone();
console.log("x" + JSON.stringify(x));
ui.helper.remove();
x.draggable({
helper: 'original',
cursor: 'move',
containment: '.droppable',
tolerance: 'fit',
drop: function(event, ui) {
$(ui.draggable).remove();
}
});
x.appendTo('.droppable');
x.addClass('clg');
$(".clg").each(function() {
//alert("hello");
jsPlumb.addEndpoint($(this), EndpointOptions);
});
}
<!-- $(".clg").dblclick(function() { -->
<!-- //alert("hello"); -->
<!-- jsPlumb.addEndpoint($(this), EndpointOptions); -->
<!-- }); -->
jsPlumb.bind('connection', function(e) {
jsPlumb.select(e).addOverlay(["Arrow", {
foldback: 0.2,
location: 0.65,
width: 25
}]);
});
console.log("out x" + JSON.stringify(x));
}
});
});
You can use this jsPlumb.repaintEverything();
Here I'm generating an id for each block and adding endpoints based upon respective id.
Here is my changes:
if(null == ui.draggable.attr('id')){
if( ui.draggable.attr('class').indexOf('rule') != -1){
clone.attr('id', 'rule_' + i);
jsPlumb.addEndpoint(clone,{anchors: ["Left"]}, EndpointOptions);
} else {
clone.attr('id', 'event_' + i);
jsPlumb.addEndpoint(clone, {anchors: ["Left"]}, EndpointOptions);
}
i++;

Can't get the autocomplete search form to work

I'm implementing a search form that displays suggestions as you start typing but can't get it to work..the problem is that when you start typing it doesn't shows any suggestion. Can you help me to get the code right? Many thanks!
This is the code:
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js"></script>
<div><input id="autocomplete" type="text"></div>
<script>
$("input#autocomplete").autocomplete({
source: [
{ id : "Google", value : "Google"},
{ id : "Yahoo", value : "Yahoo"},
],
minLength: 1,
open: function(event, ui) {
$("ul.ui-autocomplete").unbind("click");
var data = $(this).data("autocomplete");
console.log(data);
for(var i=0; i<=data.options.source.length-1;i++)
{
var s = data.options.source[i];
$("li.ui-menu-item a:contains(" + s.value + ")").attr("href", "/" + s.id);
}
}
});
/*
$("input#autocomplete").bind("autocompleteselect", function(event, ui) {
//alert(ui.item.id + ' - ' + ui.item.value);
//document.location.href = ui.item.id + '/' + ui.item.value;
//event.preventDefault;
} );
*/
</script>
Here Is the code:
<div id="search">
<input list="results" id="project" onkeydown="if (event.keyCode == 13) { checkInput(this.value); return false; }" />
</div>
The avaible results...
<datalist id="results">
<option>Demo</option>
<option>Example</option>
<option>pizza</option>
</datalist>
Finally the javascript
function checkInput(searchQuery)
{
if(searchQuery=="Home")
{
window.location = "Home.html";
}
else if(searchQuery == "Contact")
{
window.location = "Contact.html";
}
else if(searchQuery == "Sitemap")
{
window.location = "Sitemap.html";
}
else
{
window.location = "noresult.html";
}
}
So that way when ever someone goes to search they have a limited amount of options in the pre-populated list and which ever one they select leads them to your target page! I can't take all the credit, but I hope that helps!