ArcGIS Online - Order Popups same as Legend

22410
89
11-02-2012 03:01 PM
Status: Implemented
BrianSullivan
New Contributor III

When you have overlapping map layers that have popups, there is a bar at the top that allows you to toggle to the next layer.  0EME0000000TTcH

But there is no way to control the order of which popup is displayed first.  Please give control to this, or make popup come up in the same order as the legend.

89 Comments
MaryMillus

How many votes, likes, and posted comments do we need before ESRI looks into this issue?

by Anonymous User

Best thing to do is to call support, request an enhancement and to also contact your Account Manager and tell them this is an issue

BrianFausel

Here's another specific example where this enhancement would help me: I have a Utility-focused app that shows Sanitary/Storm/Water Main lines and Parcels. I have pop-ups enabled for all 4 of these layers. Because it is a Utility-focused app, I want the Main pop-ups to have the priority.

Because the Parcel popups always load faster, they show up first and it confuses some of our users. Compounding this problem is that we have "stacked parcels" for condos, and in those areas you have to cycle through dozens of popups to get to the Main one.

I understand the argument that we should create focused apps for different purposes and limit the number of pop-ups in any given app. In this case, we need the ability to have pop-ups for Mains and Parcels and would be willing to trade the minor speed increase for the enhanced control. Having one app to look up parcel data and another to look up utility data is not an option.

by Anonymous User

So this is how I accomplished this.

Note that this is probably unsupported, but it gets the job done for my needs. It will impart a minor performance hit which will scale up as more layers are included in your Web Map. I've removed support for related records, however. I can't guarantee this will work with other custom apps / customization, but it seems stable.  

I did this in 2.7 WAB Dev edition. 

I made a custom function inside the 'PopupManager.js' file located in the apps/ID/jimu.js folder. Specifically, I included a function that intercepts the popup.setFeatures method, reorders the features, and recalls the original popup.setFeaturesmethod.

To have this as the default behavior in all  future apps, you could drop this in the client/stemapp/jimu.js folder.

Below is the custom PopupManager.js file's contents. Here is a link showing the differences between the default file and the customize file: Saved diff K7Gfq1lI - Diff Checker 

For the safest implementation, create a new file in your app's jimu.js folder called 'PopupManager_NEW.js' and paste the following code:

///////////////////////////////////////////////////////////////////////////
// Copyright © 2015 Esri. All Rights Reserved.
//
// Licensed under the Apache License Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///////////////////////////////////////////////////////////////////////////
define([
  'dojo/_base/declare',
  'dojo/_base/lang',
  'dojo/_base/html',
  'dojo/topic',
  'dojo/on',
  'dojo/query',
  './FeatureActionManager',
  './utils',
  './dijit/FeatureActionPopupMenu',
  'dojo/_base/array',
  'dojo/promise/all',
  'dojo/Deferred',
  'dojo/aspect',
  './LayerStructure'
  ], function(declare, lang, html, topic, on, query, FeatureActionManager,
  jimuUtils, PopupMenu, array, all, Deferred, aspect, LayerStructure) {
    var instance = null;
    var clazz = declare(null, {
      mapManager: null,
      // popupUnion = {
      //   mobile: is mobile popup of map,
      //   bigScreen: is popup of map
      // };
      popupUnion: null,

      constructor: function(options) {
        lang.mixin(this, options);

        this.popupMenu = PopupMenu.getInstance();
        this.isInited = false;

        this.featureActionManager = FeatureActionManager.getInstance();
        topic.subscribe("mapLoaded", lang.hitch(this, this.onMapLoadedOrChanged));
        topic.subscribe("mapChanged", lang.hitch(this, this.onMapLoadedOrChanged));
        topic.subscribe("appConfigChanged", lang.hitch(this, this._onAppConfigChanged));
        topic.subscribe("widgetsActionsRegistered", lang.hitch(this, this._onWidgetsActionsRegistered));
      },

      init: function() {
        this.popupUnion = this.mapManager.getMapInfoWindow();
        if(!this.popupUnion.bigScreen || !this.popupUnion.mobile ||
          !this.popupUnion.bigScreen.domNode || !this.popupUnion.mobile.domNode){
          return;
        }
        if(!this.isInited){
          this._createPopupMenuButton();
          this._bindSelectionChangeEvent();
          this.isInited = true;
        }

        this.reorderPopupFeatures();
      },

      reorderPopupFeatures: function() {
        var that = this;
        this.layerStructrue = LayerStructure.getInstance();
        // intercept the popup.setFeatures method, reorder feautres and recall original popup.setFeatures method.
        aspect.around(this.popupUnion.bigScreen, "setFeatures", function(originalSetFeatures) {
          return function(featuresArg, options) {
            var convertedFeatureDefs = [];

            // having to consider that there are two categories of features parameter can be received.
            //  1, feature array
            //  2, deferred array
            array.forEach(featuresArg, function(featureOrDef) {
              if(featureOrDef.declaredClass === "esri.Graphic") {
                // it is a feature
                var def = new Deferred();
                def.resolve([featureOrDef]);
                convertedFeatureDefs.push(def);
              } else {
                // it is a deferred
                convertedFeatureDefs.push(featureOrDef);
              }
            });

            this.clearFeatures();
            all(convertedFeatureDefs).then(lang.hitch(this, function(results) {
              var features = [];
              array.forEach(results, function(result) {
                array.forEach(result, function(feature) {
                  if(feature) {
                    // remove duplicated features
                    var featureAlreadyExist = array.some(features, function(f) {
                      if(feature === f) {
                        return true;
                      } else {
                        return false;
                      }
                    });

                    if(!featureAlreadyExist) {
                      features.push(feature);
                    }
                  }
                });
              }, this);

              // sort features by layers order.
              var orderedFeatures = that.sortFeatures(features);

              // recall origin setFeatures.
              originalSetFeatures.apply(this, [orderedFeatures, options]);
            }));

          };
        });

        // js-api will using options.closetFirst paramether to show popup by default when clicking the map.
        // this paramether will impact features order, so having to deny it.
        // that means the closeFirst parameter will never tack effect for show popup in the WAB environment.
        aspect.around(this.popupUnion.bigScreen, 'show', function(originalShow) {
          return function(location/*, options*/) {
            originalShow.apply(this, [location, false]);
          };
        });
      },

      // accordiing to layers order to sort features
      sortFeatures: function(features) {
        if(!this.layerOrderPriority) {
          // according to layers order to define a priority object, using to sort features.
          this.layerOrderPriority = {};
          var priority = 1;
          this.layerStructrue.traversal(lang.hitch(this, function(layerNode) {
            this.layerOrderPriority[layerNode.id] = priority++;
          }));
        }

        // update this.layerOrderPriority if the layer structure has been changed.
        if(!this.layerStructureChangeHandler) {
          this.layerStructureChangeHandler = this.layerStructrue.on('structure-change', lang.hitch(this, function() {
            this.layerOrderPriority = null;
          }));
        }

        array.forEach(features, function(feature) {
          if(feature && feature.getLayer) {
            feature._priority = this.layerOrderPriority[feature.getLayer().id];
          } else {
            feature._priority = 100000;
          }
        }, this);

//       features.sort(function(featureA, featureB) {
//         return featureA._priority > featureB._priority;
//        });
          
          features.sort(function(featureA, featureB) {
               if(featureA._priority > featureB._priority)
               { return 1;}
               else
               { return -1;}
               return 0;
               });

        return features;
      },

      _createPopupMenuButton: function(){
        if(this.popupMenuButtonDesktop) {
          html.destroy(this.popupMenuButtonDesktop);
        }
        if(this.popupMenuButtonMobile) {
          html.destroy(this.popupMenuButtonMobile);
        }
        this.popupMenuButtonDesktop = html.create('span', {
          'class': 'popup-menu-button'
        }, query(".actionList", this.popupUnion.bigScreen.domNode)[0]);

        var mobileActionListNode =
          query("div.esriMobileInfoView.esriMobilePopupInfoView .esriMobileInfoViewItem").parent()[0];
        var mobileViewItem = html.create('div', {
            'class': 'esriMobileInfoViewItem'
          }, mobileActionListNode);
        this.popupMenuButtonMobile = html.create('span', {
          'class': 'popup-menu-button'
        }, mobileViewItem);

        on(this.popupMenuButtonMobile, 'click', lang.hitch(this, this._onPopupMenuButtonClick));
        on(this.popupMenuButtonDesktop, 'click', lang.hitch(this, this._onPopupMenuButtonClick));
      },

      _onPopupMenuButtonClick: function(evt){
        var position = html.position(evt.target);
        this.popupMenu.show(position);
      },

      _bindSelectionChangeEvent: function(){
        on(this.popupUnion.bigScreen, "selection-change", lang.hitch(this, this._onSelectionChange));
        on(this.popupUnion.mobile, "selection-change", lang.hitch(this, this._onSelectionChange));
      },

      _onSelectionChange: function(evt){
        this.selectedFeature = evt.target.getSelectedFeature();
        if(!this.selectedFeature){
          this._disablePopupMenu();
          return;
        }
        this.initPopupMenu([this.selectedFeature]);

        var selectedFeatureLayer = this.selectedFeature.getLayer();
        var hasInfoTemplate = this.selectedFeature.infoTemplate ||
                              (selectedFeatureLayer && selectedFeatureLayer.infoTemplate);
      },

      _disablePopupMenu: function() {
        html.addClass(this.popupMenuButtonDesktop, 'disabled');
        html.addClass(this.popupMenuButtonMobile, 'disabled');
      },

      _enablePopupMenu: function() {
        html.removeClass(this.popupMenuButtonDesktop, 'disabled');
        html.removeClass(this.popupMenuButtonMobile, 'disabled');
      },

      // public method, can be called from outside.
      initPopupMenu: function(features){
        if(!features) {
          this._disablePopupMenu();
          this.popupMenu.setActions([]);
          return;
        }
        var featureSet = jimuUtils.toFeatureSet(features);
        this.featureActionManager.getSupportedActions(featureSet).then(lang.hitch(this, function(actions){
          var excludeActions = ['ZoomTo', 'ShowPopup', 'Flash', 'ExportToCSV',
            'ExportToFeatureCollection', 'ExportToGeoJSON',
            'SaveToMyContent', 'CreateLayer'];
          var popupActions = actions.filter(lang.hitch(this, function(action){
            return excludeActions.indexOf(action.name) < 0 ;
          }));

          if(popupActions.length === 0){
            this._disablePopupMenu();
          }else{
            this._enablePopupMenu();
          }
          var menuActions = popupActions.map(lang.hitch(this, function(action){
            //action.data = jimuUtils.toFeatureSet(feature);
            action.data = featureSet;
            return action;
          }));
          this.popupMenu.setActions(menuActions);
        }));
      },

      /******************************
       * Events
       ******************************/
      onMapLoadedOrChanged: function() {
        this.isInited = false;
        this.init();
      },

      _onAppConfigChanged: function() {
        if(this.popupUnion) {
          if(this.popupUnion.bigScreen && this.popupUnion.bigScreen.hide) {
            this.popupUnion.bigScreen.hide();
            this.popupMenu.hide();
          }
          if(this.popupUnion.mobile && this.popupUnion.mobile.hide) {
            this.popupUnion.mobile.hide();
            this.popupMenu.hide();
          }
        }
      },

      _onWidgetsActionsRegistered: function(){
        //to init actions
        this.init();
      },

      /**********************************
       * Methods for show related records
       **********************************/


    });

    clazz.getInstance = function(mapManager) {
      if (instance === null) {
        instance = new clazz({
          mapManager: mapManager
        });
      }
      return instance;
    };

    return clazz;
  });

Save this file. Rename the existing file named 'PopupManager.js' to 'PopupManager_BAK.js.' Then rename the file you named 'PopupManager_NEW.js' to 'PopupManager.js'

JohnTownsend

This is an important feature and I hope it gets implemented soon.  In my case, I have a layer on top that is editable.  There are other supporting layers that have useful information for a user to reference but not the information I would like to display by default.  As it stands, each time we want to make an edit on the top layer they must navigate through the popups to find the editable layer.  This is a frustrating and inefficient way of doing things.  The alternative is to disable the popup or turn off the layer manually.  Not an ideal solution.  This seems like a pretty essential option to have control over how data is displayed and interacted with. 

by Anonymous User

Hey John!

If you make the above modification to your PopUpManager.js file as I indicated above, you'll be able to achieve this

KevinRobicheau

I would be willing to make users wait 3 seconds for proper pop-up ordering.

How about an honor pop-up order for _____ seconds setting, after that go with the fastest.

It solves the possibility of a major slowdown that Esri is worried about, and allows for a spectrum of compromise in the user experience.

MuhammadAlmas2

Wow. This request was made in 2012, and now 2018 is about to end and still we are in need of this functionality. What a performance by ESRI. 
If you don't want to implement any functionality, then why not to inform the users that they will not get it ???

StaceyCurry1

It's 2019 and this still hasn't been addressed? We need this functionality to have custom pop-ups for our users who are not GIS Professionals to get this information quickly. This will always be needed.

StaceyCurry1

How do you begin going about this? I am unfamiliar with JSON.

Many thanks!