Prevent drop of duplicate nodes from Palette

Hi,

I’ve searched through the forum and tried a lot of things, but can’t seem to find a good solution…

I have two diagrams, palette and main. When a node is dropped from palette onto the main diagram (either empty space or another node), I’d like to be able to detect whether the node being dropped is already in the main diagram, hence preventing duplicate nodes. Simply deleting selection from palette is not enough for my use case, as palette will be reloaded and some nodes in the palette might already be included in the main diagram from before.

I tried using both mouseDrop of node template as well as “ExternalObjectsDropped” handler for the main diagram as suggested in another thread and following the Flowgrammer example.
In the example below, palette nodes with keys 1, 2, 3 are already on the main diagram, therefore dropping them onto the main diagram would introduce duplicates which is what I am trying to avoid.

Any hints or tips are greatly appreciated.
Cheers,

<!doctype html>
<html>
<head>
    <title>Palette Test</title>
    <script src="go-debug.js"></script>
    <script id="code">
        function init() {
            var $ = go.GraphObject.make; // for conciseness in defining templates
           // initialize the main Diagram
            var diagram =
                $(go.Diagram, "diagramContainer", // must be the ID or reference to div
                    {
                        initialContentAlignment: go.Spot.Center,
                        // make sure users can only create trees
                        validCycle: go.Diagram.CycleDestinationTree,
                        // users can select only one part at a time
                        maxSelectionCount: 1,

                        layout: $(go.TreeLayout, {
                            treeStyle: go.TreeLayout.StyleLastParents,
                            arrangement: go.TreeLayout.ArrangementHorizontal,
                            // properties for most of the tree:
                            angle: 90,
                            layerSpacing: 35,
                            // properties for the "last parents":
                            alternateAngle: 90,
                            alternateLayerSpacing: 35,
                            alternateAlignment: go.TreeLayout.AlignmentBus,
                            alternateNodeSpacing: 20
                        }),
                        // enable undo & redo
                        "undoManager.isEnabled": true,
                        allowDragOut: false,
                        allowDrop: true
                    });

            diagram.nodeTemplate =
                $(go.Node, "Auto",
                {
                    mouseDragEnter: function(e, node, prev) {
                        var diagram = node.diagram;
                        var selnode = diagram.selection.first();

                        if (!canBeChild(selnode, node)) return;
                    },
                    mouseDrop: dropOntoNode
                },
                $(go.Shape, "RoundedRectangle", {
                        fill: "white"
                    },
                    new go.Binding("fill", "color"), {
                        cursor: "pointer"
                    }),
                $(go.TextBlock, {
                        margin: 5
                    },
                    new go.Binding("text", "color"))
        );
        diagram.model.nodeDataArray = [{
            key: 1,
            color: "lightblue"
        }, 
        {
            key: 2,
            color: "orange"
        }, {
            key: 3,
            color: "yellow"
        }, {
            key: 4,
            color: "red"
        }, {
            key: 5,
            color: "gray"
        }];

        diagram.model.linkDataArray = [{"from":1, "to":2}, {"from":1, "to":3},{"from":2, "to":4}, {"from":1, "to":5}    ];
        function canBeChild(node1, node2) {
            if (!(node1 instanceof go.Node)) return false; // must be a Node
            if (node1 === node2) return false; // cannot be child of yourself
            if (node2.isInTreeOf(node1)) return false; // already have a parent
            return true;
        }


        // start off with no Parts
        diagram.undoManager.isEnabled = true;

        // create the Palette
        var palette =
            $(go.Palette, "myPaletteDiv", { maxSelectionCount: 1});

        // the Palette's node template is different from the main Diagram's
        palette.nodeTemplate =
            $(go.Node, "Horizontal",
                $(go.Shape, {
                        width: 14,
                        height: 14,
                        fill: "white"
                    },
                    new go.Binding("fill", "color")),
                $(go.TextBlock,
                    new go.Binding("text", "color"))
            );

        // the list of data to show in the Palette
        palette.model.nodeDataArray = [{
            key: 1,
            color: "lightblue"
        },{
            key: 2,
            color: "orange"
        },{
            key: 3,
            color: "yellow"
        }, {
            key: 4,
            color: "purple"
        }, {
            key: 5,
            color: "green"
        }];

        palette.model.undoManager = diagram.model.undoManager;
        
        diagram.addDiagramListener("ExternalObjectsDropped", function(e) {            
            var newnode = palette.selection.first(); //always dropping from palette
            var dupe = isDupe(newnode.data);
            if(dupe){ //already in diagram, remove the duplicate
                diagram.commandHandler.deleteSelection();
                }
            else{ // new node to add to diagram
                palette.commandHandler.deleteSelection(); //reomve from palette
                }
        });
        
        
        function dropOntoNode(e, node) {
            var diagram = node.diagram;
            var selnode = diagram.selection.first(); // assume just one Node in selection
            // should do a duplicate check?
            if (canBeChild(selnode, node)) {
                // find any existing link into the selected node
                var link = selnode.findTreeParentLink();
                if (link !== null) { // reconnect any existing link
                    link.fromNode = node;
                } else { // else create a new link
                    diagram.toolManager.linkingTool.insertLink(node, node.port, selnode, selnode.port);
                }
            }
        }
        
        function isDupe(data){
            var el = diagram.findNodeForData(data);
            var b  = el != null;
            return b; 
        }
    }
    </script>
</head>

<body onload="init()">
    <div>
        <span id="paletteSpan" style="display: inline-block; vertical-align: top">
    <b>Palette:</b><br>
    <div id="myPaletteDiv" style="width: 120px; height: 250px; position: relative; cursor: auto; border : solid 1px blue;" class="diagramContainer"></div>
  </span>
        <span id="diagramSpan" style="display: inline-block; vertical-align: top">
    <b>Diagram:</b><br>
  <div class="diagramContainer" style="width: 350px; height: 250px; position: relative; cursor: auto; border : solid 1px blue;" id="diagramContainer"></div>
  </span>
    </div>
</body>

</html>

At least two problems:
1) Your listener is looking at the palette.selection – it should look at diagram.selection.
2) isDupe is looking at whether a node data that is in the palette.model is in the diagram.model – it should search through all of the diagram.nodes to see if there are any with whatever properties that you care about.

Thanks, Walter. I made a few modifications.

  • previously key values were exactly identical between palette and diagram (copy/paste problem), so everything really was a duplicate.
  • now using id as key property
  • now checking diagram.selection in isDupe
  • now using diagram.nodes iterator and comparing with id property in isDupe
  • isDupe2 alternative duplicate check using containsNodeData

After the changes, all dropped nodes are incorrectly identified as duplicate

  • if using diagram.selection in isDupe instead of palette.selection, since some palette keys are the same, when dropping a
    duplicate node, it would not keep its key value, but would generate a unique one
  • so a palette node with duplicate key 1 would become -6. So should I still use diagram.selection or change back to palette.selection? How to handle duplicate keys?
  • it seems that by the time ExternalObjectsDropped is called, the node being dropped is already part of diagram, the element is already in the collection and thus will always appear to be duplicate.
    I am posting the modified sample.
    Thanks once again.

[code]<!doctype html>

Palette Test function init() { var $ = go.GraphObject.make; // for conciseness in defining templates // initialize the main Diagram var diagram = $(go.Diagram, "diagramContainer", // must be the ID or reference to div { initialContentAlignment: go.Spot.Center, // make sure users can only create trees validCycle: go.Diagram.CycleDestinationTree, // users can select only one part at a time maxSelectionCount: 1,
                    layout: $(go.TreeLayout, {
                        treeStyle: go.TreeLayout.StyleLastParents,
                        arrangement: go.TreeLayout.ArrangementHorizontal,
                        // properties for most of the tree:
                        angle: 90,
                        layerSpacing: 35,
                        // properties for the "last parents":
                        alternateAngle: 90,
                        alternateLayerSpacing: 35,
                        alternateAlignment: go.TreeLayout.AlignmentBus,
                        alternateNodeSpacing: 20
                    }),
                    // enable undo & redo
                    "undoManager.isEnabled": true,
                    allowDragOut: false,
                    allowDrop: true
                });

        diagram.nodeTemplate =
            $(go.Node, "Auto",
            {
                mouseDragEnter: function(e, node, prev) {
                    var diagram = node.diagram;
                    var selnode = diagram.selection.first();

                    if (!canBeChild(selnode, node)) return;
                },
                mouseDrop: dropOntoNode
            },
            $(go.Shape, "RoundedRectangle", {
                    fill: "white"
                },
                new go.Binding("fill", "color"), {
                    cursor: "pointer"
                }),
            $(go.TextBlock, {
                    margin: 5
                },
                new go.Binding("text", "color"))
    );
    diagram.model.nodeKeyProperty = "id";
    diagram.model.nodeDataArray = [{
        id: 1,
        color: "lightblue"
    }, {
        id: 2,
        color: "orange"
    }, {
        id: 3,
        color: "yellow"
    }, {
        id: 4,
        color: "red"
    }, {
        id: 5,
        color: "gray"
    }];

    diagram.model.linkDataArray = [{"from":1, "to":2}, {"from":1, "to":3},{"from":2, "to":4}, {"from":1, "to":5}    ];
    function canBeChild(node1, node2) {
        if (!(node1 instanceof go.Node)) return false; // must be a Node
        if (node1 === node2) return false; // cannot be child of yourself
        if (node2.isInTreeOf(node1)) return false; // already have a parent
        return true;
    }


    // start off with no Parts
    diagram.undoManager.isEnabled = true;

    // create the Palette
    var palette =
        $(go.Palette, "myPaletteDiv", { maxSelectionCount: 1});

    // the Palette's node template is different from the main Diagram's
    palette.nodeTemplate =
        $(go.Node, "Horizontal",
            $(go.Shape, {
                    width: 14,
                    height: 14,
                    fill: "white"
                },
                new go.Binding("fill", "color")),
            $(go.TextBlock,
                new go.Binding("text", "color"))
        );

    palette.model.nodeKeyProperty = diagram.model.nodeKeyProperty;
    
    // the list of data to show in the Palette
    palette.model.nodeDataArray = [{
        id: 1, //duplicate id
        color: "lightblue"
    },{
        id: 2, //duplicate id
        color: "orange"
    },{
        id: 3, //duplicate id
        color: "yellow"
    }, {
        id: 10, //different id
        color: "purple"
    }, {
        id: 11, //different id
        color: "green"
    }];

    palette.model.undoManager = diagram.model.undoManager;
    
    diagram.addDiagramListener("ExternalObjectsDropped", function(e) {            
        var newnode = diagram.selection.first(); //always dropping from palette
        var key = newnode.data.id;
        var dupe = isDupe(key);
        // var dupe = isDupe2(newnode.data);
        alert("dupe found: " + dupe);
        if(dupe){ //already in diagram, remove the duplicate node
            alert("removing dupe from main diagram...");
            diagram.commandHandler.deleteSelection();
            }
        else{ // new node to add to diagram
            alert("added node to main diagram, deleting from palette...");
            palette.commandHandler.deleteSelection(); //reomve node from palette
            }
    });
    
    
    function dropOntoNode(e, node) {
        var diagram = node.diagram;
        var selnode = diagram.selection.first(); // assume just one Node in selection
        // should do a duplicate check?
        if (canBeChild(selnode, node)) {
            // find any existing link into the selected node
            var link = selnode.findTreeParentLink();
            if (link !== null) { // reconnect any existing link
                link.fromNode = node;
            } else { // else create a new link
                diagram.toolManager.linkingTool.insertLink(node, node.port, selnode, selnode.port);
            }
        }
    }
    
    function isDupe2(data){
        alert("trying to find dupe key " + data.id);
        var contains = diagram.model.containsNodeData(data);
        return contains;
    }
    function isDupe(key){
        alert("trying to find dupe key " + key);
        var nodes = diagram.nodes;
        while (nodes.next()) {
            var n = nodes.value;
            var nd = n.data;
            var id = nd.id;
            if(id === key)
                return true;
        }
        return false;
    }
    
}
</script>
Palette:
Diagram:
[/code]

That’s right – as documented, that DiagramEvent is raised after the drop has happened, so that you can delete or modify any newly created Parts, created due to the newly copied data in the model.

So you cannot depend on the key (“id”) values, since they will be unique within the model/diagram, and are probably different from what they were in the source palette model/diagram.

Ahhh, well duh… I’ve been over-thinking this all along… So just use a non-key property to do logic check for duplicate! Simple enough, however that introduced another question.

  • now that I have id as model node key, and objId as the logical node object ID, how to get links to go by objId instead of id?
    As always, I truly appreciate all your help.

What do you mean by “how to get links to go by objId instead of id?”

By default with a GraphLinksModel:
$(go.GraphLinksModel,
{
nodeDataArray: [
{ key: 1, text: “one” },
{ key: 2, text: “two” }
],
linkDataArray: [
{ from: 1, to: 2 }
]
})

With your node key change:
$(go.GraphLinksModel,
{
nodeKeyProperty: “id”,
nodeDataArray: [
{ id: 1, text: “one” },
{ id: 2, text: “two” }
],
linkDataArray: [
{ from: 1, to: 2 }
]
})

Maybe you’re asking about changing the names of the “from” and “to” properties? Such as:
$(go.GraphLinksModel,
{
nodeKeyProperty: “id”,
linkFromKeyProperty: “f”,
linkToKeyProperty: “t”,
nodeDataArray: [
{ id: 1, text: “one” },
{ id: 2, text: “two” }
],
linkDataArray: [
{ f: 1, t: 2 }
]
})

I apologize for not being clearer earlier…

Since the diagram model will assign a unique value to the node key when dropped (in case of integers, it seems that is negative value of node count, such as -6, for example, or concatenated value in case of strings), I modified the model as follows

{ "class": "go.GraphLinksModel", "nodeKeyProperty": "id", "linkFromKeyProperty": "f", "linkToKeyProperty": "t", "nodeDataArray": [ {"id":1, "objId":10001, "color":"lightblue"}, {"id":2, "objId":10002, "color":"orange"}, {"id":3, "objId":10003, "color":"yellow"}, {"id":4, "objId":10004, "color":"red"}, {"id":5, "objId":10005, "color":"gray"} ], "linkDataArray": [ {"f":1, "t":2}, {"f":1, "t":3}, {"f":2, "t":4}, {"f":1, "t":5} ]}
Here objId is the actual object ID we care about, retrieved from DB or API call, and it is used in determining duplicates.
I would like to establish a link between nodes representing objId 10001 and objId 10002, regardless of what their diagram/model keys are, since the keys could be autogenerated, but objId’s are not. Also, since I plan on retaining link data, it would be logical to have something like

<span style="line-height: 16.7999992370605px;"> </span><span style="line-height: 16.7999992370605px;">"linkDataArray": [ </span><div style="line-height: 16.7999992370605px;">{"f":<span style="line-height: 16.7999992370605px;">10001 </span>, "t":<span style="line-height: 16.7999992370605px;">10002</span>},<div style="line-height: 16.7999992370605px;">{"f":<span style="line-height: 16.7999992370605px;">10001 </span>, "t":<span style="line-height: 16.7999992370605px;">1000</span><span style="line-height: 16.7999992370605px;">3},</span><div style="line-height: 16.7999992370605px;">{"f":2<span style="line-height: 16.7999992370605px;">0001 </span><span style="line-height: 16.7999992370605px;">, "t":</span><span style="line-height: 16.7999992370605px;">1000</span><span style="line-height: 16.7999992370605px;">4},</span><div style="line-height: 16.7999992370605px;">{"f":<span style="line-height: 16.7999992370605px;">10001</span><span style="line-height: 16.7999992370605px;">, "t":</span><span style="line-height: 16.7999992370605px;">1000</span><span style="line-height: 16.7999992370605px;">5}</span> <span style="line-height: 16.7999992370605px;"> ]</span>

Again, node data and link data are to be read in via API calls and subsequent modifications are to be stored. The restriction is that each objId appears only once in the main diagram, hence the thread about preventing duplicates.


Apologies for lack of clarity in the last post. Sometimes code snippets can explain what I am trying to do better than I can with words.
Cheers

It seems to me that you should not use a separate “objId” property. The “key”/“id” should be whatever number or string your database expects to be using.

So the keys for newly created node data will be whatever the Model assigns. (If you want, you can customize it by setting Model.makeUniqueKeyFunction.) Then whenever you find out what key the database is actually using for that node data object, you can call Model.setKeyForNodeData.

A post was split to a new topic: Conditionally adding model data