Select to view content in your preferred language

Experience Builder (Developer edition)

2679
7
Jump to solution
01-04-2023 07:31 AM
Nadia_Matsiuk
Occasional Contributor

 

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!

 

0 Kudos
1 Solution

Accepted Solutions
JamesGough
Regular Contributor

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

 

View solution in original post

7 Replies
JamesGough
Regular Contributor

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>
      );
    }
}
Nadia_Matsiuk
Occasional Contributor

@JamesGoughThanks 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]").

0 Kudos
JamesGough
Regular Contributor

Can you post your full widget.tsx code?

0 Kudos
Nadia_Matsiuk
Occasional Contributor

Yeah, of course. Thanks a lot.

/** @jsx jsx */
import { React, AllWidgetProps, jsx } from 'jimu-core'
import { JimuMapViewComponent, JimuMapView } from "jimu-arcgis";
import Point from "esri/geometry/Point";
import Popup from 'esri/widgets/Popup';
import PopupTemplate from 'esri/PopupTemplate';

export default class Widget extends React.PureComponent<AllWidgetProps<any>, any>{

    state = {
      jimuMapView: null,
      latitude: "",
      longitude: "",
      timer: null,
      PopupTemplate: {
        title: "{state_abbr}",
        content: "{state_name}"
      }
    };

    setLatLong = (lat: number, long: number) => {
      this.setState({
        latitude: lat.toFixed(3),
        longitude: long.toFixed(3),
        PopupTemplate: {
          title: "{state_abbr}",
          content: "{state_name}"
        }
      });
    }

    activeViewChangeHandler = (jmv: JimuMapView) => {
      if (jmv) {
        this.setState({
          jimuMapView: jmv
        });

        jmv.view.on("pointer-move", (evt) => {
          if(this.state.timer){
            clearTimeout(this.state.timer)
          }
          jmv.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;
              jmv.view.popup.open({
                location: graphic.geometry.centroid,
                features: [graphic]
              });
            } else {
              jmv.view.popup.close();
            }
          });

          this.setState({
            timer: setTimeout(this.setLatLong, 0, point.latitude, point.longitude)
          })
        });
      }
    };


    render() {
      return (
        <div className="widget-hoverPopup jimu-widget">
          {this.props.hasOwnProperty("useMapWidgetIds") &&
            this.props.useMapWidgetIds &&
            this.props.useMapWidgetIds[0] && (
              <JimuMapViewComponent
                useMapWidgetId={this.props.useMapWidgetIds?.[0]}
                onActiveViewChange={this.activeViewChangeHandler}
              />
            )
          }
          <p>Latitude/Longtitude/hoverPopup: {this.state.latitude} {this.state.longitude}</p>
        </div>
      );
    }
}
0 Kudos
JamesGough
Regular Contributor

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

 

Nadia_Matsiuk
Occasional Contributor

Wow! It's worked. Thanks a lot!

0 Kudos
Nadia_Matsiuk
Occasional Contributor

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

 

 
Thanks a lot!
0 Kudos