Hide Table of Contents
View Editor with undo redo sample in sandbox
Editor with undo redo

Description

This sample shows how to use the UndoManager, a utility object, that allows you to incoporate undo/redo functionality into your application. When you create the UndoManager you can specify the number of operations that will be maintained on the undo/redo stack.

undoManager = new esri.UndoManager({maxOperations:8});

There are several out-of-the-box operations: Add, Delete, Update, Cut and Union. You can also create custom operations by inheriting from the OperationBase class. In this sample we use the Add, Delete and Update operations to maintain a stack of feature edits. This code snippet shows how to add a new operation to the stack. When existing features are updated using the applyEdits method add the update to the stack.

layer.applyEdits(null, [feature], null, function() {
var operation = new esri.dijit.editing.Update({
featureLayer
: layer,
preUpdatedGraphics
: [new esri.Graphic(originalFeature)],
postUpdatedGraphics
: [feature]
});

undoManager
.add(operation);
checkUI
();
});
});

If the application end-user wants to undo the update they click the undo button which calls the UndoManager's undo method.
<button id="undo" disabled="true" onclick="undoManager.undo();" dojoType="dijit.form.Button" iconClass="undoIcon" >Undo</button>

Code

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
    <title>UndoManager</title>

    <link rel="stylesheet" href="https://js.arcgis.com/3.24/dijit/themes/claro/claro.css">
    <link rel="stylesheet" href="https://js.arcgis.com/3.24/esri/css/esri.css">
    <style>
      html, body {
        height: 100%;
        width: 100%;
        margin: 0; 
        padding: 0;
        overflow:hidden;
      }
      .instructions{
        padding-top:20px;
        font-size:12px;
      }
      .undoButtons{
        width:60%;
        margin-left:auto;
        margin-right:auto;
        padding-top:4px;
      }
      #map{
        padding:0px;
        border:solid 2px #1A84AD;
        -moz-border-radius: 4px;
        border-radius: 4px;
      }
      #rightPane{
        border:none;
        width:300px;
      }
      .templatePicker {
        border:solid 2px #1A84AD !important;
      }
      .undoIcon { background-image:url(images/undo.png); width:16px; height:16px; }
      .redoIcon { background-image:url(images/redo.png); width:16px; height:16px;}
    </style>

    <script>var dojoConfig = { parseOnLoad: true };</script>
    <script src="https://js.arcgis.com/3.24/"></script>
    <script>
      dojo.require("dijit.layout.BorderContainer");
      dojo.require("dijit.layout.ContentPane");
      dojo.require("esri.map");
      dojo.require("esri.undoManager");
      dojo.require("esri.dijit.editing.TemplatePicker-all");
      dojo.require("esri.dijit.AttributeInspector-all");
      dojo.require("esri.dijit.editing.editOperation");

      var map;
      var undoManager;
      var attributeInspector;

      function init() {
        //specify the number of undo operations allowed using the maxOperations parameter
        undoManager = new esri.UndoManager({ maxOperations: 8 });

        //listen for the undo/redo button click events
        dojo.connect(dojo.byId('undo'), 'onclick', function(e) {
          undoManager.undo();
        });
        dojo.connect(dojo.byId('redo'), 'onclick', function(e) {
          undoManager.redo();
        });

        //This sample requires a proxy page to handle communications with the ArcGIS Server services. You will need to
        //replace the url below with the location of a proxy on your machine. See the 'Using the proxy page' help topic
        //for details on setting up a proxy page.

        esri.config.defaults.io.proxyUrl = "/proxy/";
        esri.config.defaults.io.alwaysUseProxy = false;

        map = new esri.Map("map", {
          basemap: "topo",
          center: [-97.367, 37.691],
          zoom: 14
        });

        dojo.connect(map, "onLayersAddResult", initEditing);

        var landuseLayer = new esri.layers.FeatureLayer("https://sampleserver6.arcgisonline.com/arcgis/rest/services/Military/FeatureServer/6", {
          mode: esri.layers.FeatureLayer.MODE_SNAPSHOT,
          outFields: ["*"]
        });

        map.addLayers([landuseLayer]);

        //display the attribute inspector in the info window.
        map.infoWindow.setContent("<div id='attributesDiv'></div>");
        map.infoWindow.resize(300, 190);
      }

      function initEditing(results) {
        var layer = results[0].layer;
        var layers = dojo.map(results, function(result) {
          return result.layer;
        });

        var layerInfos = dojo.map(results, function(result) {
          return {featureLayer: result.layer, isEditable: true, showAttachments: false};
        });

        //Ctrl+click to delete features and add this delete operation to undomanager
        dojo.connect(layer, "onClick", function(evt) {
          dojo.stopEvent(evt);

          if (evt.ctrlKey === true || evt.metaKey === true) {  //delete feature if ctrl key is depressed
            layer.applyEdits(null, null, [evt.graphic], function() {
              var operation = new esri.dijit.editing.Delete({
                featureLayer: layer,
                deletedGraphics: [evt.graphic]
              });

              undoManager.add(operation);
              checkUI();
            });
          }
        });

        dojo.connect(layer, "onBeforeApplyEdits", function() {
          dijit.byId("undo").set("disabled", true);
          dijit.byId("redo").set("disabled", true);
        });

        dojo.connect(layer, "onEditsComplete", function(adds, updates, deletes) {
          //display attribute inspector for newly created features
          if (adds.length > 0) {
            var query = new esri.tasks.Query();
            query.objectIds = [adds[0].objectId];
            layer.selectFeatures(query, esri.layers.FeatureLayer.SELECTION_NEW, function(features) {
              if (features.length > 0) {
                var screenPoint = map.toScreen(features[0].geometry);
                //display the attribute window for newly created features
                map.infoWindow.setTitle("");
                map.infoWindow.show(screenPoint, map.getInfoWindowAnchor(screenPoint));
              }
              else {
                map.infoWindow.hide();
              }
            });
          }
          if (deletes.length > 0) {
            //hide the info window if features are deleted.
            map.infoWindow.hide();
          }
          checkUI();
        });

        //Add the attribute inspector and listen for events to update feature layer
        //when attributes are modified.
        var attInspector = new esri.dijit.AttributeInspector({layerInfos: layerInfos}, "attributesDiv");

        //delete the feature and close the info window if displayed.
        dojo.connect(attInspector, "onDelete", function(feature) {
          var layer = feature.getLayer();
          layer.applyEdits(null, null, [feature], function() {
            var operation = new esri.dijit.editing.Delete({
              featureLayer: layer,
              deletedGraphics: [feature]
            });

            undoManager.add(operation);
            checkUI();
          });
          layer.clearSelection();
          map.infoWindow.hide();
        });

        //show the info window for the next selected feature
        dojo.connect(attInspector, "onNext", function(feature) {
          var screenPoint = map.toScreen(feature.geometry.getExtent().getCenter());
          map.infoWindow.show(screenPoint, map.getInfoWindowAnchor(screenPoint));
        });

        //Update the feature service attributes and add each attribute change to
        //the undo manager for undo/redo capability
        dojo.connect(attInspector, "onAttributeChange", function(feature, fieldName, newFieldValue) {
          var originalFeature = feature.toJson();
          feature.attributes[fieldName] = newFieldValue;

          var layer = feature.getLayer();
          layer.applyEdits(null, [feature], null, function() {
            var operation = new esri.dijit.editing.Update({
              featureLayer: layer,
              preUpdatedGraphics: [new esri.Graphic(originalFeature)],
              postUpdatedGraphics: [feature]
            });

            undoManager.add(operation);
            checkUI();
          });
        });

        var templatePicker = new esri.dijit.editing.TemplatePicker({
          featureLayers: layers,
          rows: "auto",
          columns: 3,
          grouping: true
        }, "templatePickerDiv");
        templatePicker.startup();

        var drawToolbar = new esri.toolbars.Draw(map);

        var selectedTemplate;
        //when users select an item from the template picker activate the draw toolbar
        //with the geometry type of the selected template item.
        dojo.connect(templatePicker, "onSelectionChange", function() {
          if (templatePicker.getSelected()) {
            selectedTemplate = templatePicker.getSelected();
          }
          drawToolbar.activate(esri.toolbars.Draw.POINT);
        });

        //once the geometry is drawn - call applyEdits to update the feature service with the new geometry
        dojo.connect(drawToolbar, "onDrawEnd", function(geometry) {
          drawToolbar.deactivate();

          var newAttributes = dojo.mixin({}, selectedTemplate.template.prototype.attributes);
          var newGraphic = new esri.Graphic(geometry, null, newAttributes);
          //when features are added - add them to the undo manager
          selectedTemplate.featureLayer.applyEdits([newGraphic], null, null, function() {
            var operation = new esri.dijit.editing.Add({
              featureLayer: selectedTemplate.featureLayer,
              addedGraphics: [newGraphic]
            });

            undoManager.add(operation);

            checkUI();
          });
        });
      }

      //disable or enable undo/redo buttons depending on current app state
      function checkUI() {
        if (undoManager.canUndo) {
          dijit.byId("undo").set("disabled", false);
        }
        else {
          dijit.byId("undo").set("disabled", true);
        }

        if (undoManager.canRedo) {
          dijit.byId("redo").set("disabled", false);
        }
        else {
          dijit.byId("redo").set("disabled", true);
        }
      }

      dojo.ready(init);
    </script>
  </head>
  <body class="claro">
    <div data-dojo-type="dijit.layout.BorderContainer" data-dojo-props="gutters:true, design:'sidebar'" style="width:100%;height:100%;">
      <div id="map" data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region:'center'"></div>
      <div id="rightPane" data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region:'right'">
        <div id="templatePickerDiv"></div>
        <div class="undoButtons">
         <button id="undo"  data-dojo-type="dijit.form.Button" data-dojo-props="disabled:true, iconClass:'undoIcon'" >Undo</button>
         <button id="redo"  data-dojo-type="dijit.form.Button" data-dojo-props="disabled:true, iconClass:'redoIcon'" >Redo</button>
        </div>
        <div class='instructions'>
          <ul style="list-style:none;padding-left:4px;">
            <li><b>Create Features:</b> Select template then click on map.</li>
            <li><b>Delete Features:</b> Ctrl or Cmd + Click feature.</li>
          </ul>
          The undo/redo buttons will become enabled after editing the feature attributes or geometry.
        </div>
      </div>
    </div>
  </body>
</html>
 
          
Show Modal