Popup always open in the marker - react-leaflet

Is there any way the popup always stays open and does not need to click on it to open?
Expected behaviour
Actual behavior

With the introduction of react-leaflet version 2 which brings breaking changes in regard of creating custom components, it is no longer supported to extend components via inheritance (refer this thread for a more details)
In fact React official documentation also recommends to use composition instead of inheritance:
At Facebook, we use React in thousands of components, and we haven’t
found any use cases where we would recommend creating component
inheritance hierarchies.
Props and composition give you all the flexibility you need to
customize a component’s look and behavior in an explicit and safe way.
Remember that components may accept arbitrary props, including
primitive values, React elements, or functions.
The following example demonstrates how to extend marker component in order to keep popup open once the marker is displayed:
const MyMarker = props => {
const initMarker = ref => {
if (ref) {
ref.leafletElement.openPopup()
}
}
return <Marker ref={initMarker} {...props}/>
}
Explanation:
get access to native leaflet marker object (leafletElement) and open popup via Marker.openPopup method
Here is a demo

What you can do is to make your own Marker class from the react-leaflet marker, and then call the leaflet function openPopup() on the leaflet object after it has been mounted.
// Create your own class, extending from the Marker class.
class ExtendedMarker extends Marker {
componentDidMount() {
// Call the Marker class componentDidMount (to make sure everything behaves as normal)
super.componentDidMount();
// Access the marker element and open the popup.
this.leafletElement.openPopup();
}
}
This will make the popup open once the component has been mounted, and will also behave like a normal popup afterwards, ie. on close/open.
I threw together this fiddle that shows the same code together with the basic example.

You can use permanent tooltips, or React provides refs for this type of thing... you can do this:
https://jsfiddle.net/jrcoq72t/121/
const React = window.React
const { Map, TileLayer, Marker, Popup } = window.ReactLeaflet
class SimpleExample extends React.Component {
constructor () {
super()
this.state = {
lat: 51.505,
lng: -0.09,
zoom: 13
}
}
openPopup (marker) {
if (marker && marker.leafletElement) {
window.setTimeout(() => {
marker.leafletElement.openPopup()
})
}
}
render () {
const position = [this.state.lat, this.state.lng]
return (
<Map center={position} zoom={this.state.zoom}>
<TileLayer attribution='© OpenStreetMap contributors' url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" />
<Marker position={position} ref={this.openPopup}>
<Popup>
<span>
A pretty CSS3 popup. <br /> Easily customizable.
</span>
</Popup>
</Marker>
</Map>
)
}
}
window.ReactDOM.render(<SimpleExample />, document.getElementById('container'))
References:
https://reactjs.org/docs/refs-and-the-dom.html
React.js - access to component methods
Auto open markers popup on react-leaflet map

The above no longer works with react-leaflet version 3. In your custom marker component, to get a reference to the leaflet element you should now use useRef() and then open up the popup in useEffect() once the component is mounted.
const MyMarker = (props) => {
const leafletRef = useRef();
useEffect(() => {
leafletRef.current.openPopup();
},[])
return <Marker ref={leafletRef} {...props} />
}

For the new react-leaflet v4 you will need to do some changes
const CustomMarker = ({ isActive, data, map }) => {
const [refReady, setRefReady] = useState(false);
let popupRef = useRef();
useEffect(() => {
if (refReady && isActive) {
map.openPopup(popupRef);
}
}, [isActive, refReady, map]);
return (
<Marker position={data.position}>
<Popup
ref={(r) => {
popupRef = r;
setRefReady(true);
}}
>
{data.title}
</Popup>
</Marker>
);
};
And then use MapContainer like this
const MapComponent = () => {
const [map, setMap] = useState(null);
return (
<div>
<MapContainer
ref={setMap}
center={[45.34416, 15.49005]}
zoom={15}
scrollWheelZoom={true}
>
<CustomMarker
isActive
map={map}
data={{
position: [45.34416, 15.49005],
title: "Text displayed in popup",
}}
/>
</MapContainer>
</div>
);
};

Edited
Notice that my old solution would try to open the popup every time it renders.
Found another solution that fit my needs to open it when the position changed. Notice that I look at position.lat, position.lng since it will think it always changes if you pass on the object.
And yes it is not perfect typescript but it is the best solution I could come up with.
const CustomMarker: React.FC<CustomMarkerProps> = ({ position, children }) => {
const map = useMap();
const markerRef = useRef(null);
useEffect(() => {
try {
// #ts-ignore
if (markerRef.current !== null && !markerRef.current.isPopupOpen()) {
// #ts-ignore
markerRef.current.openPopup();
}
} catch (error) {}
}, [position.lat, position.lng]);
return (
<Marker ref={markerRef} position={position}>
<Popup>{children}</Popup>
</Marker>
);
};
export default CustomMarker;
Old solution
Could not get it to work using useRef and useEffect. However, got it to work with calling openPopup() directly from the ref.
import { LatLngLiteral, Marker as LMarker } from "leaflet";
import React from "react";
import { Marker } from "react-leaflet";
export interface CustomMarkerProps {
position: LatLngLiteral;
open?: boolean;
}
const CustomMarker: React.FC<CustomMarkerProps> = ({
position,
open = false,
children,
}) => {
const initMarker = (ref: LMarker<any> | null) => {
if (ref && open) {
ref.openPopup();
}
};
return (
<Marker ref={initMarker} position={position}>
{children}
</Marker>
);
};
export default CustomMarker;

Related

Multiple Doughnut chart as markers over Openstreetmap

I'm in trouble while implementing the doughnut chart over OpenStreetMap. I'm using react-chartjs2 for the doughnut chart and react-leaflet for Openstreetmap. Like we use the location icon on different coordinates over the map but here I want to use a Doughnut graph over the map instead of the location icon.
I want to achieve something like this
As per the react-leaflet documentation, the Marker icon property accepts two types of icons that is icon strings like image URL and divIcon which can be some HTML elements but while I'm rendering react component it does not accept and not showing it.
Here you can check in codesandbox I have added code to make it easy to try
https://codesandbox.io/s/doughnut-chart-over-osm-map-1indvl?file=/src/App.js
For what I know marker Icons can only be static, I use a function to create my only markers based on icons and plain html. Will be hard to do that with a component in your case.
My icon render function
import { divIcon } from "leaflet";
import { ReactElement } from "react";
import { renderToString } from "react-dom/server";
export const createLeafletIcon = (
icon: ReactElement,
size: number,
className?: string,
width: number = size,
height: number = size
) => {
return divIcon({
html: renderToString(icon),
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height],
className: className ? className : "",
});
};
In your case I would try to cheese it and create blank markers and show the graph in popups instead and just force the popups to alway stay open.
EDIT: Added my custom Marker code below that have some nice options.
You can just use the defaultOpen option, and add the graph as a child component to the marker and it will show up in the popup. You can the change the styling of you liking to make it look like the graph is the marker.
import { LatLngLiteral } from "leaflet";
import React, { Children, ReactElement, useEffect, useRef } from "react";
import { Marker, Popup, useMap } from "react-leaflet";
import { MapPin } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
export interface LeafletMarkerProps {
position: LatLngLiteral;
flyToPosition?: boolean;
size?: number;
color?: string;
icon?: ReactElement;
defaultOpen?: boolean;
onOpen?: () => void;
children?: React.ReactNode;
markerType?: string;
zIndexOffset?: number;
}
const LeafletMarker: React.FC<LeafletMarkerProps> = ({
position,
flyToPosition = false,
children,
size = 30,
color,
defaultOpen = false,
onOpen,
icon = <MapPin size={size} color={color} />,
markerType,
zIndexOffset,
}) => {
const map = useMap();
const markerRef = useRef(null);
position && flyToPosition && map.flyTo(position);
const markerIcon = createLeafletIcon(icon, size, markerType); // Important to not get default styling
useEffect(() => {
if (defaultOpen) {
try {
// #ts-ignore
if (markerRef.current !== null && !markerRef.current.isPopupOpen()) {
// #ts-ignore
markerRef.current.openPopup();
}
} catch (error) {}
}
}, [defaultOpen, position.lat, position.lng]);
return (
<Marker
eventHandlers={{
popupopen: () => onOpen && onOpen(),
}}
ref={markerRef}
icon={markerIcon}
position={position}
zIndexOffset={zIndexOffset}
>
{/* autoPan important to not have jittering */}
<Popup autoPan={false}>{children}</Popup>
</Marker>
);
};
export default LeafletMarker;

Change the center of React-leaflet

I am trying to dynamically change the center of a map-container with data provided externally. I get the data as a string, and then parse it to get it as numbers instead. But when I enter lat to the const center, I get a NaN when trying use it.
import React from 'react'
import { useCasparData } from 'caspar-graphics'
import { useTimeline } from '#nxtedition/graphics-kit'
import './style.css'
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'
import './leaflet.css'
export default function Lowerthird () {
const { text01, text02, text03, text04 } = useCasparData()
const lat = parseFloat(text01)
const zoom = 15
const center = [lat, 13.440222]
function onLoad(timeline) {
timeline
.add('start')
.from('.name', { x: -2000 }, 'start')
.from('.titel', { x: -1000 }, 'start')
}
function onStop(timeline) {
timeline
.reverse()
}
//useTimeline(onLoad, onStop)
return (
<MapContainer center={center} zoom={zoom} zoomControl={false}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
)
}
export const previewData = {
text01: '59.392133',
text02: '13.440222',
text03: '15',
text04: '[59.392133, 13.440222]'
}
I have looked through several threads here, but I have not found a answer that solves this for me... I do realize that the map-container is immutable - I just can't seem to figure out how to update it or set a new center...
(Oh... I am a total noob to react/leaflet, I am just trying to find a simple way to use Openstreetmap as a overlay in our broadcasts (tv))
You can use useMap() from a child component.
And with map, you can use functions such as flyTo to move around on the map.
Sadly useMap needs to be used from the child scope of MapContainer, however, you can just create an invisible component and add as a child and provide it with the new position when you want it to move.
So something like this (I have not tested this code but I have done similar stuff with markers):
interface ChangeCenterProps {
position: { lat: number; lng: number };
}
const ChangeCenter: React.FC<ChangeCenterProps> = ({ position }) => {
const map = useMap();
map.flyTo(position);
return <></>;
};

Delay on the capture of an image with React Native Camera / Expo Camera, set 'processing message'?

I have a huge delay in my app from when a user takes a photo to when android processes the image.
The issue of delay in Expo with android camera has been answered here: Delay on the capture of an image - React Native Camera / Expo Camera.
I am trying to see if, as well as see skipProcessing on takePictureAsync, is there a way to set a callback for on processing? I would like to let the user know something is happening, or they may try to take another photo (the camera stays open until the image has been processed, which is not ideal).
Here is my code:
export default class CameraComponent extends Component{
constructor(props) {
super(props)
}
render() {
<Camera
ref={(ref) => {
this.camera = ref;
}}
type={Camera.Constants.Type.back}
>
<TouchableOpacity
onPress={this.takePicture}
>
<View>
<FontAwesome
name="camera"
/>
</View>
</TouchableOpacity>
</Camera>;
}
takePicture = async () => {
const photo = await this.camera.takePictureAsync({
skipProcessing: true,
});
this.props.navigation.navigate("CameraConfirm", {
img_url: photo.img_url,
img_base64: photo.img_base64,
});
}
}
I can't see anything in the docs, is there maybe a way around in React Native? I tried setting state, but that still happens after takePictureAsync so has no effect.
I found one workaround which uses camera2API and onPictureSaved. The docs say there could be issues with camera2API, although I did not see anything weird going on (as yet).
The code now looks like this:
export default class CameraComponent extends Component{
constructor(props) {
super(props)
this.state = {
is_photo_taken: false,
};
}
render() {
<Camera
ref={(ref) => {
this.camera = ref;
}}
type={Camera.Constants.Type.back}
// add styling to show/hide camera:
style={{
display: this.state.is_photo_taken ? "none" : null,
}}
useCamera2Api={true} // add this line
>
<TouchableOpacity
onPress={this.takePicture}
>
<View>
<FontAwesome
name="camera"
/>
</View>
</TouchableOpacity>
</Camera>;
// add loading element with conditional show/hide:
<Text
style={{
display: !this.state.is_photo_taken ? "none" : null,
}}
>
Loading image...
</Text>
}
takePicture = async () => {
const photo = await this.camera.takePictureAsync({
onPictureSaved: this.setState({ is_photo_taken: true }), // add this line
// remove skipProcessing
});
this.props.navigation.navigate("CameraConfirm", {
img_url: photo.img_url,
img_base64: photo.img_base64,
});
}
}
Also, since onPictureSaved is now being used, it means that skipProcesssing can now be omitted, if not needed.
I used show/hide instead of ternaries or && around the entire block to avoid losing the camera element from the page. If the camera element were to be lost as soon as a photo was taken, then it could not continue and process the image.
I hope this helps someone.

Extract function to standalone custom React component in react-leaflet

My primary goal is to call fitBounds whenever a FeatureGroup is rendered in react-leaflet on initial load.
This renders correctly -
<Map>
<LayersControl>
{getLayers(groups)}
</LayersControl>
</Map>
function getLayers(featureGroups: MyFeatureGroup[]){
const showOnLoad = true;
return featureGroups.map((group: MyFeatureGroup) => {
const groupRef = createRef<FeatureGroup>();
const { id, name, } = group;
return (
<LayersControl.Overlay checked={showOnLoad} key={id} name={name}>
<FeatureGroup ref={groupRef}>
<Layer {...group} />
</FeatureGroup>
</LayersControl.Overlay>
);
});
}
However, because it is using a function instead of React component, I don't have access to using React hooks.
The alternative that I tried does not work, even though it is the same code wrapped in a React component -
...same as above...
return featureGroups.map((group: MyFeatureGroup) => (
<ControlledGroup {...group} showOnLoad={showOnLoad} /> ///----- > ADDED THIS HERE
));
const ControlledGroup: React.FC<ControlledGroupProps> = (props) => {
const groupRef = createRef<FeatureGroup>();
const { map } = useLeaflet();
/// -----> map is correctly defined here - injecting to all of the layers (LayersControl, FeatureGroup) does not solve the problem
const { showOnLoad, ...group } = props;
useEffect(() => fitBounds(map, groupRef)); ///-----> Primary Goal of what I am trying to accomplish
return (
<LayersControl.Overlay
checked={showOnLoad}
key={group.id}
name={name}
>
<FeatureGroup ref={groupRef}>
<Layer map={map} {...group} />
</FeatureGroup>
</LayersControl.Overlay>
);
};
I am a bit stumped, since this is the same code. The getLayers function returns a ReactNode in both cases. However, when moving to a standalone ControlledGroup component, it throws an error on render -
addOverlay is not a function
I tried creating a custom class component for react-leaflet, but the difficulty that I ran into there is that createLeafletElement returns a Leaflet.Element, whereas I am simply looking to return a ReactNode. That is, all of these are valid react-leaflet components already.
My questions - why does one work and the other does not? What is the correct/recommended way to convert this function to a renderable stand-alone React component?
Further, if there is an alternative pattern to calling fitBounds, that would be helpful as well.
Any insight would be appreciated.
Since the Layers share an inheritance with Layers.Overlay, the solution to the render error is to keep the Layers together and move the feature group to a standalone component.
This works as expected and allows me to call useEffect on the groupRef -
function getLayers(groups: MyFeatureGroup[]){
return featureGroups.map((group: MyFeatureGroup) => {
const { id, name, } = group;
return (
///---> Keep the Overlay in the function here and extract just the FeatureGroup out
<LayersControl.Overlay checked={showOnLoad} key={id} name={name}>
<ControlledGroup {...group}></ControlledGroup>
</LayersControl.Overlay>
);
}

Draftail mention plugin for wagtail

I would like to customise the draftail editor in wagtail in order to have "mentions" functionality (I am using the draft js plugin)
Because my react skills are extremely poor and my knowledge of draftail/draftjs very limited, I am using https://github.com/vixdigital/wagtail-plugin-base as a starting point (without knowing much about what it does) and trying to muddle through the task
I have managed to get it so the mention functionality appears in wagtail. The problem is that it appears in a "sub" editor in the toolbar
enter image description here
How can I avoid creating a sub editor and have it work in the body of the existing editor? Below are the two main JS files in my "wagtail-plugin-base"
index.js
import AutoComplete from './AutoComplete/AutoComplete.js';
window.draftail.registerControl(AutoComplete)
AutoComplete.js
import React, { Component } from '../../node_modules/react';
import createMentionPlugin, { defaultSuggestionsFilter } from '../../node_modules/draft-js-mention-plugin';
import editorStyles from './editorStyles.css';
import { mentions } from './mentions'
import { DraftailEditor, BLOCK_TYPE, INLINE_STYLE, createEditorState } from "draftail"
const mentionPlugin = createMentionPlugin();
const AutoComplete = class SimpleMentionEditor extends Component {
state = {
editorState: createEditorState,
suggestions: mentions,
};
onChange = (editorState) => {
this.setState({
editorState,
});
};
onSearchChange = ({ value }) => {
this.setState({
suggestions: defaultSuggestionsFilter(value, mentions),
});
};
onAddMention = () => {
// get the mention object selected
}
focus = () => {
this.editor.focus();
};
render() {
const { MentionSuggestions } = mentionPlugin
return (
<div>
<DraftailEditor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={[mentionPlugin]}
ref={(element) => { this.editor = element; }}
/>
<MentionSuggestions
onSearchChange={this.onSearchChange}
suggestions={this.state.suggestions}
onAddMention={this.onAddMention}
/>
</div>
);
}
}
export default AutoComplete;
Any pointers much appreciated!