Hi!
I need for some help, please.
Can I create hover popup in Experience builder (Developer edition)?
I know code what can do hover popup. But I don't know in wich place and wich file it's need to add.I read a lot of articles, but still don't know the answer.
Maybe it's can be done in place where map is initialize. But I can't found it's place in code.
Thanks for any help!
Solved! Go to Solution.
In your code featureLayer is undefined. If there is some specific feature layer you are trying to reference, you need should add another dataSourceSelector in setting.tsx and then use that datasource to get the feature layer. Also, you aren't doing the hitTest in the timer callback, so it is just going to run whenever you are move the mouse.
Here is a rough solution. For the timer callback we call a new function named doHitTest which runs a hitTest on the mapview using the pointer event. A popup is displayed with all of the layer graphics which intercept the map geometry at that point.
/** @jsx jsx */
import { React, AllWidgetProps, jsx } from 'jimu-core'
import { JimuMapViewComponent, JimuMapView } from "jimu-arcgis";
export default class Widget extends React.PureComponent<AllWidgetProps<any>, any>{
state = {
jimuMapView: null,
timer: null
};
doHitTest = (evt: PointerEvent) => {
if(this.state.jimuMapView){
// Run hitTest on the map view using the pointer event
this.state.jimuMapView.view.hitTest(evt).then((response) => {
if(response.results.length > 0){
const graphicResults = []; // array to hold graphic results
// Loop through the hitTest results
for(const r of response.results){
// If r has a GraphicHit
if(r.graphic){
// add it to the array of graphics
graphicResults.push(r.graphic)
}
}
if(graphicResults.length > 0){
// Open popup with the features from graphicResults
this.state.jimuMapView.view.popup.open({
location: graphicResults[0].geometry.centroid,
features: graphicResults
});
}
}
})
}
}
activeViewChangeHandler = (jmv: JimuMapView) => {
if (jmv) {
this.setState({
jimuMapView: jmv
});
// register the pointer-move event handler on the map view
jmv.view.on("pointer-move", (evt) => {
// if there is already a timer, cancel it
if(this.state.timer){
clearTimeout(this.state.timer)
}
// set new timer that will call doHitTest after the interval
// pass the pointer-move event as the parameter to doHitTest
this.setState({
timer: setTimeout(this.doHitTest, 2000, evt)
})
});
}
};
render() {
return (
<div className="widget-starter jimu-widget">
{this.props.hasOwnProperty("useMapWidgetIds") &&
this.props.useMapWidgetIds &&
this.props.useMapWidgetIds[0] && (
<JimuMapViewComponent
useMapWidgetId={this.props.useMapWidgetIds?.[0]}
onActiveViewChange={this.activeViewChangeHandler}
/>
)
}
</div>
);
}
}
You can create a custom widget and in the widget code you can implement this behavior.
Check out the experience builder tutorials for Create a start widget and Get map coordinates. The second tutorial shows how to add a "pointer-move" event handler on the map view. In your widget you can use this same handler, when a user moves their cursor you can set a timer using setTimeout() , if cursor is moved again before timeout end you can cancel the timeout and set a new one. Otherwise, once timeout ends, you can run your identify and display a popup.
Here is a modified version of the Get Map Coordinates sample which shows the lat long only after hovering over a spot for 2 seconds:
// widget.tsx
/** @jsx jsx */
import { React, AllWidgetProps, jsx } from 'jimu-core'
import { JimuMapViewComponent, JimuMapView } from "jimu-arcgis";
import Point from "esri/geometry/Point";
export default class Widget extends React.PureComponent<AllWidgetProps<any>, any>{
state = {
jimuMapView: null,
latitude: "",
longitude: "",
timer: null
};
setLatLong = (lat: number, long: number) => {
this.setState({
latitude: lat.toFixed(3),
longitude: long.toFixed(3)
});
}
activeViewChangeHandler = (jmv: JimuMapView) => {
if (jmv) {
this.setState({
jimuMapView: jmv
});
jmv.view.on("pointer-move", (evt) => {
if(this.state.timer){
clearTimeout(this.state.timer)
}
const point: Point = this.state.jimuMapView.view.toMap({
x: evt.x,
y: evt.y
});
this.setState({
timer: setTimeout(this.setLatLong, 2000, point.latitude, point.longitude)
})
});
}
};
render() {
return (
<div className="widget-starter jimu-widget">
{this.props.hasOwnProperty("useMapWidgetIds") &&
this.props.useMapWidgetIds &&
this.props.useMapWidgetIds[0] && (
<JimuMapViewComponent
useMapWidgetId={this.props.useMapWidgetIds?.[0]}
onActiveViewChange={this.activeViewChangeHandler}
/>
)
}
<p>Lat/Lon: {this.state.latitude} {this.state.longitude}</p>
</div>
);
}
}
@JamesGough, Thanks a lot!
I use this fragment of code for popup:
view.on("pointer-move", function (event) { view.hitTest(event).then(function (response) { if (response.results.length) { var graphic = response.results.filter(function (result) { // check if the graphic belongs to the layer of interest return result.graphic.layer === featureLayer; })[0].graphic; view.popup.open({ location: graphic.geometry.centroid, features: [graphic] }); } else { view.popup.close(); } }); });
I tried adaptate him to your example, but can't do it in the right way (get issue with "featureLayer" or "[graphic]").
Can you post your full widget.tsx code?
Yeah, of course. Thanks a lot.
In your code featureLayer is undefined. If there is some specific feature layer you are trying to reference, you need should add another dataSourceSelector in setting.tsx and then use that datasource to get the feature layer. Also, you aren't doing the hitTest in the timer callback, so it is just going to run whenever you are move the mouse.
Here is a rough solution. For the timer callback we call a new function named doHitTest which runs a hitTest on the mapview using the pointer event. A popup is displayed with all of the layer graphics which intercept the map geometry at that point.
/** @jsx jsx */
import { React, AllWidgetProps, jsx } from 'jimu-core'
import { JimuMapViewComponent, JimuMapView } from "jimu-arcgis";
export default class Widget extends React.PureComponent<AllWidgetProps<any>, any>{
state = {
jimuMapView: null,
timer: null
};
doHitTest = (evt: PointerEvent) => {
if(this.state.jimuMapView){
// Run hitTest on the map view using the pointer event
this.state.jimuMapView.view.hitTest(evt).then((response) => {
if(response.results.length > 0){
const graphicResults = []; // array to hold graphic results
// Loop through the hitTest results
for(const r of response.results){
// If r has a GraphicHit
if(r.graphic){
// add it to the array of graphics
graphicResults.push(r.graphic)
}
}
if(graphicResults.length > 0){
// Open popup with the features from graphicResults
this.state.jimuMapView.view.popup.open({
location: graphicResults[0].geometry.centroid,
features: graphicResults
});
}
}
})
}
}
activeViewChangeHandler = (jmv: JimuMapView) => {
if (jmv) {
this.setState({
jimuMapView: jmv
});
// register the pointer-move event handler on the map view
jmv.view.on("pointer-move", (evt) => {
// if there is already a timer, cancel it
if(this.state.timer){
clearTimeout(this.state.timer)
}
// set new timer that will call doHitTest after the interval
// pass the pointer-move event as the parameter to doHitTest
this.setState({
timer: setTimeout(this.doHitTest, 2000, evt)
})
});
}
};
render() {
return (
<div className="widget-starter jimu-widget">
{this.props.hasOwnProperty("useMapWidgetIds") &&
this.props.useMapWidgetIds &&
this.props.useMapWidgetIds[0] && (
<JimuMapViewComponent
useMapWidgetId={this.props.useMapWidgetIds?.[0]}
onActiveViewChange={this.activeViewChangeHandler}
/>
)
}
</div>
);
}
}
Wow! It's worked. Thanks a lot!
@JamesGough , Hi!
Can you give some advice, please?
You are right. I want reference to specific feature layer. So I change setting.tsx and get opportunities to choose feature layer from my Map Widget (I want create pop up what may be able for specific layer, because now hover pop up active for all graphic layers). But hover popup doesn't work with it.
Can you tell more about DataSourceSelector? For example how to get the feature layer for hover popup?
There are widget.tsx and setting.tsx files in attachment (.zip with hoverPopup widget). Also this is my setting.tsx change:
import { React, Immutable, IMFieldSchema, UseDataSource } from 'jimu-core'
import { AllWidgetSettingProps } from 'jimu-for-builder'
import { MapWidgetSelector } from 'jimu-ui/advanced/setting-components'
import {DataSourceSelector, AllDataSourceTypes, FieldSelector} from 'jimu-ui/advanced/data-source-selector';
import { render } from '@testing-library/react';
import { WebMapDataSourceImpl } from 'jimu-arcgis/arcgis-data-source';
export default function Setting(props: AllWidgetSettingProps<{}>){
const onMapWidgetSelected = (useMapWidgetIds: string[]) => {
props.onSettingChange({
id: props.id,
useMapWidgetIds: useMapWidgetIds
});
}
const onFieldChange = (allSelectedFields: IMFieldSchema[]) => {
props.onSettingChange({
id: props.id,
useDataSources: [{...props.useDataSources[0], ...{fields: allSelectedFields.map(f => f.jimuName)}}]
});
}
const onToggleUseDataEnabled = (useDataSourcesEnabled: boolean) => {
props.onSettingChange({
id: props.id,
useDataSourcesEnabled
});
}
const onDataSourceChange = (useDataSources: UseDataSource[]) => {
props.onSettingChange({
id: props.id,
useDataSources: useDataSources,
});
}
return (
<div className="widget-setting-demo">
<MapWidgetSelector
useMapWidgetIds={props.useMapWidgetIds}
onSelect={onMapWidgetSelected}
/>
<DataSourceSelector
types={Immutable([AllDataSourceTypes.FeatureLayer])}
useDataSources={props.useDataSources}
useDataSourcesEnabled={props.useDataSourcesEnabled}
onToggleUseDataEnabled={onToggleUseDataEnabled}
onChange={onDataSourceChange}
widgetId={props.id}
/>
{
props.useDataSources && props.useDataSources.length > 0 &&
<FieldSelector
useDataSources={props.useDataSources}
onChange={onFieldChange}
selectedFields={props.useDataSources[0].fields || Immutable([])}
/>
}
</div>
)
};