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 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 Update({

featureLayer: layer,

preUpdatedGraphics: [new 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.
registry.byId("undo").on("click", function(e) {
undoManager.undo();
});
<button id="undo" data-dojo-type="dijit/form/Button" data-dojo-props="disabled:true, 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.46/dijit/themes/claro/claro.css">
   
<link rel="stylesheet" href="https://js.arcgis.com/3.46/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;
        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 src="https://js.arcgis.com/3.46/"></script>
   
<script>
     
var map, undoManager, attInspector;
      require
([
       
"esri/map",
       
"esri/layers/FeatureLayer",
       
"esri/undoManager",
       
"esri/dijit/AttributeInspector",
       
"esri/dijit/editing/TemplatePicker",

       
"esri/dijit/editing/Add",
       
"esri/dijit/editing/Delete",
       
"esri/dijit/editing/Update",
       
"esri/dijit/editing/Editor",
       
"esri/tasks/query",
       
"esri/toolbars/draw",
       
"esri/graphic",

       
"dojo/parser",
       
"dojo/_base/event",
       
"dijit/registry",
       
"dojo/_base/array",

       
"dijit/form/Button",
       
"dijit/layout/BorderContainer",
       
"dijit/layout/ContentPane",
       
"dojo/domReady!"
     
], function(
       
Map, FeatureLayer, UndoManager, AttributeInspector, TemplatePicker,
       
Add, Delete, Update, Editor, Query, Draw, Graphic,
        parser
, event, registry, array
     
) {
        parser
.parse();


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

       
// listen for the undo/redo button click events
        registry
.byId("undo").on("click", function(e) {
          undoManager
.undo();
       
});
        registry
.byId("redo").on("click", function(e) {
          undoManager
.redo();
       
});

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

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

        map
.addLayers([landuseLayer]);
        map
.on("layers-add-result", initEditing);

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

         
var layerInfos = array.map(results.layers, function(result) {
           
return {featureLayer: results.layers[0].layer, isEditable: true, showAttachments: false};
         
});

         
//Ctrl+click to delete features and add this delete operation to undomanager
          layer
.on("click", function(evt) {
            event
.stop(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 Delete({
                  featureLayer
: layer,
                  deletedGraphics
: [evt.graphic]
               
});
                undoManager
.add(operation);
                checkUI
();
             
});
           
}
         
});

          layer
.on("before-apply-edits", function() {
            dijit
.byId("undo").set("disabled", true);
            dijit
.byId("redo").set("disabled", true);
         
});

          layer
.on("edits-complete", function(evt) {
           
//display attribute inspector for newly created features
           
if (evt.adds.length > 0) {
             
var query = new Query();
              query
.objectIds = [evt.adds[0].objectId];
              layer
.selectFeatures(query, 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 (evt.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.
          attInspector
= new AttributeInspector({layerInfos: layerInfos}, "attributesDiv");

         
//display the attribute inspector in the info window.
          map
.infoWindow.setContent(attInspector.domNode);
          map
.infoWindow.resize(300, 190);

         
//delete the feature and close the info window if displayed.
          attInspector
.on("delete",function(evt){
           
var feature = evt.feature;
           
var layer = feature.getLayer();
            layer
.applyEdits(null, null, [feature], function() {
             
var operation = new Delete({
                featureLayer
: layer,
                deletedGraphics
: [feature]
             
});
              undoManager
.add(operation);
              checkUI
();
           
});
            layer
.clearSelection();
            map
.infoWindow.hide();
         
});

         
//show the info window for the next selected feature
          attInspector
.on("next", function(evt) {
           
var feature = evt.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
          attInspector
.on("attribute-change", function(evt) {
           
var feature = evt.feature;
            feature
.attributes[evt.fieldName] = evt.newFieldValue;

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

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

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

          templatePicker
.startup();

         
var drawToolbar = new 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.
          templatePicker
.on("selection-change", function() {
           
if (templatePicker.getSelected()) {
              selectedTemplate
= templatePicker.getSelected();
           
}
            drawToolbar
.activate(Draw.POINT);
         
});

         
//once the geometry is drawn - call applyEdits to update the feature service with the new geometry
          drawToolbar
.on("draw-complete", function(evt) {
            drawToolbar
.deactivate();
           
var newAttributes = dojo.mixin({}, selectedTemplate.template.prototype.attributes);
           
var newGraphic = new Graphic(evt.geometry, null, newAttributes);
           
//when features are added - add them to the undo manager
            selectedTemplate
.featureLayer.applyEdits([newGraphic], null, null, function() {
             
var operation = new 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);
         
}
       
}

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