How to make anchor move with respect to a block dragged in jsplumb? - drag-and-drop

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++;

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();
});
});
}

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!

Why is CKEditor refusing my custom tag?

With a custom CKEditor plugin I am trying to insert a custom HTML tag, but the tag gets removed as soon as it is inserted into the editor. My plugin.js file:
CKEDITOR.plugins.add( 'tweet', {
icons: 'tweet',
init: function( editor ) {
editor.addCommand( 'insertTweet', {
allowedContent: 'tweet[:id]',
requiredContent: 'tweet',
exec: function( editor ) {
console.log('inserting');
editor.insertHtml( '<tweet :id="\'123\'" />' ); // also tried <tweet />
// editor.insertHtml( '[tweet :id="\'123\'" /]' ); // this works
}
});
editor.ui.addButton( 'tweet', {
label: 'Insert tweet',
command: 'insertTweet',
toolbar: 'insert,0'
});
}
});
The way I am adding the plugin for Bolt CMS backend:
function run() {
var extrasAdded = false;
if (typeof(CKEDITOR) == 'undefined') return;
CKEDITOR.plugins.addExternal('tweet', '/assets/plugins/tweet/plugin.js', '');
CKEDITOR.on('instanceReady', function (event, instance) {
if (extrasAdded === true) return;
var config = event.editor.config;
config.toolbar.push(
{ name: 'insert', items: [ 'tweet' ] }
);
config.extraPlugins += (config.extraPlugins ? ',' : '') + 'tweet';
config.extraAllowedContent = 'tweet'; // also tried 'tweet[:id]'
CKEDITOR.instances['body'].destroy();
CKEDITOR.replace('body', config);
extrasAdded = true;
});
}
if (document.readyState!='loading') run();
else document.addEventListener('DOMContentLoaded', run);
Can someone smart see why my tag is rejected?
So it turns out that we don't need the allowedContent or requiredContent properties in the plugin.js script. What did the trick was to tweak the editor's HTML DTD rules. In my case I got a reference to the editor in the instanceReady callback and tweeked it like this:
// name = 'tweet'
editor.filter.allow(name + "[!*]", name, true);
CKEDITOR.dtd[name] = CKEDITOR.dtd;
CKEDITOR.dtd.$empty[name] = 1; // allow self-closing tag
CKEDITOR.dtd.$blockLimit[name] = 1;
CKEDITOR.dtd.$nonEditable[name] = 1;
CKEDITOR.dtd.$object[name] = 1;
CKEDITOR.dtd.$inline[name] = 1; // $block won't work!
You can also see a full gist of it.

Form not submit

I have a edit user form. The form is loaded from a Json store with this code:
var store = Ext.create('cp.store.form.Paciente',{});
store.load({params:{idUsuario: idPaciente}});
var form = Ext.create('cp.view.form.EditPaciente',{
action: 'bin/paciente/modificar.php'
});
// note: write this lines in the controller
form.on('afterrender',function(form,idPaciente){
form.getForm().loadRecord(store.first());
form.getForm().findField('idUsuario').setValue(idPaciente);
});
var win = Ext.create('cp.view.ui.DecoratorForm',{
aTitle: 'Editar paciente',
aForm: form
});
win.show();
The load code works fine. The submit code is:
var me = this;
console.log('Submit...');
console.log(this.url);
// WHY NOT SUBMIT !!!!
this.getForm().submit({
console.log('submit !');
success: function(form,action){
if(action.result.success === true){
Ext.create('cp.view.ui.AlertOk',{mensaje:action.result.msg}).showDialog();
me.up('decoratorForm').close();
}else{
Ext.create('cp.view.ui.AlertErr',{mensaje:action.result.msg}).showDialog();
}
}
});
So, the submit code starts running. FireBug shows the first and second "console.log", and the "this.url" value is correct. But, the third "console.log" not execute, and the form not send to the server.
Firebug not says 404 error for "this.url" value.
Any ideas ?
Thanks !
Add the form definition:
Ext.define('cp.view.form.EditPaciente',{
extend: 'Ext.form.Panel',
alias: 'widget.editPaciente',
bodyPadding: '5px 5px 5px 5px',
bodyStyle: 'border: none',
fieldDefaults: {
labelWidth: 65,
labelAlign: 'top'
},
initComponent: function(){
this.url = this.action,
this.method = 'POST',
this.items = [ .... ]
this.callParent(arguments);
}
});
You cant put log statements inside object literals.
submit({ <-- This is an object literal
console.log('submit !'); <-- This can not be in an object literal
success: function(form,action){
if(action.result.success === true){
Ext.create('cp.view.ui.AlertOk',{mensaje:action.result.msg}).showDialog();
me.up('decoratorForm').close();
}else{
Ext.create('cp.view.ui.AlertErr',{mensaje:action.result.msg}).showDialog();
}
}
});

Sencha touch 2.0 and iphone : add element to panel dynamically and setActiveItem

I am trying to add dinamically a panel item to a main panel with a card layout which has initially one panel in it.
After one event (a tap) i build dinamically a new panel and I add it to the main panel, after this i try to set the new panel item like the active one via setActiveItem
Things work ok on Android but not on iphone.
Exactly i have this app.js:
Ext.Loader.setConfig({enabled: true});
Ext.setup({
viewport: {
autoMaximize: false
},
onReady: function() {
var app = new Ext.Application({
name: 'rpc',
appFolder: 'app',
controllers: ['Home'],
autoCreateViewport: false,
launch: function () {
Ext.create('Ext.Panel',{
fullscreen: true,
layout: {
type : 'card',
animation:{
type:'slide'
,duration :3000
}
},
defaults: { /*definisce le caratteristiche di default degli elementi contenuti..??*/
flex: 1
},
items:[{
title: 'Compose',
xtype: 'griglia'
}]
});
}
});
}
});
In a controller i have
.....
.....
var grigliaPan=button.up('griglia');
var mainPan=grigliaPan.up('panel');
var html=
'<img src="img/'+segnoScelto+'_big.png" />'
+'<h1>'+segnoScelto+'</h1>'
+'<p>'+previsione+'</p>'
+'</br></br>';
if (typeof mainPan.getComponent(1) == 'undefined'){
var previsioPan = Ext.widget('previsio');
previsioPan.setHtml(html);
//here i create a button for going home panel
var backButton=new Ext.Button({
ui : 'decline',
alias: 'widget.backbutton',
text: 'Home Page',
width : 150,
height:100
})
previsioPan.add(backButton);
var it=mainPan.getItems();
alert (it['keys']); //this prints : ext-griglia-1
mainPan.add(previsioPan);
var it=mainPan.getItems();
alert (it['keys']); //this prints : ext-griglia-1,ext-previsio-1
//
}
mainPan.getLayout().setAnimation({type: 'slide', direction: 'left', duration:1000});
//mainPan.setActiveItem(1);
var pree=mainPan.getAt(1);
//pree.show();
//mainPan.setActiveItemm(pree);
mainPan.setActiveItem('ext-previsio-1');
The three form of setActiveItem() are ok for Android and falls with iPhone. Can somebody please show me what is the right way to set the new active item added dinamically on the iphone ?
The problem should not be with the add() function cause i can see the new item via the getItems() added in main panel after the add().
by following code you can understand that thisCell is added later after creation of thisRow.
var thisRow = new Ext.Panel({
layout: { type: 'hbox', align: 'stretch', pack: 'center' },
id: 'row' + (rowCount + 1),
defaults: { flex: 1 }
});
// Now we need to add the cells to the above Panel:
var thisCell = new Ext.Panel({
cls: 'dashboardButton',
layout: { type: 'vbox', align: 'center', pack: 'center' },
items: [{
xtype: 'image',
src: 'some image url',
height: '70px',
width: '100px',
}]
});
thisRow.add(thisCell);
hope this will help you to achieve your desired output...