Implementing SpecialDraggingTool in TypeScript Angular issue

I am trying to impliment seatingchart sample for one of my clients project. But I cannot implement the special dragging tool which will keep all the guests with the table when I drag the table. please help.

Could you please describe more precisely what the problem is when you say that you “cannot implement the special dragging tool”? Are you unable to include that code in your app for some reason? Or do you have the code in the app but cannot create an instance of it to replace the standard ToolManager.draggingTool? Or did you install the SpecialDraggingTool but it doesn’t move the people assigned to seats at a table? Or something else?

I’ll elaborate on the issue my colleague has posted, I apologise for his vague description. We are trying to extend your table planner example to allow dragging new tables from a third pane into the center plan.

So there are 3 container div’s at this point instead of the original 2.

    <div class="col-md-2">

  <h3>Guests</h3>

  <hr>

  <div id='myGuests'></div>


</div>

<div class="col-md-8">
  <h3>Table Plan</h3>

  <hr>

  <div id='diagramDiv'></div>


</div>

<div class="col-md-2">

  <h3>Add More Tables</h3>
  <hr>
  <div id='tables'></div>


</div>

We are facing the following issues.

  1. When dragging a person on to the main plan. it also allows me to drop the person into the third box. It also allows me to drag a person back onto the people section but copies them not moves them.
  2. Tables dragged onto the diagram don’t allow guests to be seated at them. Only the pre-rendered tables that existed on page load respond to dropping guests on them.
  3. It allows me to delete guests from the guests pane and tables form the tables pane. Elements n these panes should be un-deletable.

If you can provide advice on the above at earliest opportunity that would be greatly appreciated.

I’ve fixed points 1 and 3, however point 2 is proving difficult.

ERROR TypeError: Cannot read property ‘1’ of undefined
at
Looks like its the highlightSeats function but im not sure what it is.

 highlightSeats(node, coll, show) {
if (this.isPerson(node)) {  // refer to the person's table instead
  node = node.diagram.findNodeForKey(node.data.table);
  if (node === null) { return; }
}
const it = coll.iterator;
while (it.next()) {
  const n = it.key;
  // if dragging a Table, don't do any highlighting
  if (this.isTable(n)) { return; }
}
const guests = node.data.guests;
console.log(guests);
for (const sit = node.elements; sit.next();) {
  const seat = sit.value;
  if (seat.name) {
    const num = parseFloat(seat.name);
    if (isNaN(num)) { continue; }
    const seatshape = seat.findObject('SEATSHAPE');
    if (!seatshape) { continue; }
    if (show) {
      if (guests[seat.name]) {
        seatshape.stroke = 'red';
      } else {
        seatshape.stroke = 'green';
      }
    } else {
      seatshape.stroke = 'white';
    }
  }
}

}

I tried adding a third DIV for a “myTables” Palette, and everything just worked:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Seating Chart</title>
  <meta name="description" content="A seating chart editor for assigning places at tables." />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- Copyright 1998-2019 by Northwoods Software Corporation. -->

  <style>
    /* Use a Flexbox to make the Palette/Diagram responsive */
    #myFlexDiv {
      display: -webkit-flex;
      display: flex;
      flex-flow: row wrap;
      width: 100%;
      justify-content: center;
    }

    @media (min-width: 768px) {
      #myGuests {
        width: 100px;
        height: 500px;
        margin-right: 10px;
      }

      #myDiagramDiv {
        height: 500px;
        flex: 1;
        margin-right: 10px;
      }

      #myTables {
        width: 150px;
        height: 500px;
        margin-right: 10px;
      }
    }

    @media (max-width: 767px) {
      #myGuests {
        width: 90%;
        height: 100px;
        margin-bottom: 10px;
      }

      #myDiagramDiv {
        width: 90%;
        height: 500px;
        margin-bottom: 10px;
      }

      #myTables {
        width: 90%;
        height: 100px;
        margin-bottom: 10px;
      }
    }
  </style>
  <script src="go.js"></script>
  <script src="../assets/js/goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">
    function init() {
      if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
      var $ = go.GraphObject.make;

      // Initialize the main Diagram
      myDiagram =
        $(go.Diagram, "myDiagramDiv",
          {
            allowClipboard: false,
            draggingTool: $(SpecialDraggingTool),
            rotatingTool: $(HorizontalTextRotatingTool),
            // For this sample, automatically show the state of the diagram's model on the page
            "ModelChanged": function(e) {
              if (e.isTransactionFinished) {
                document.getElementById("savedModel").textContent = myDiagram.model.toJson();
              }
            },
            "undoManager.isEnabled": true
          });

      myDiagram.nodeTemplateMap.add("",  // default template, for people
        $(go.Node, "Auto",
          { background: "transparent" },  // in front of all Tables
          // when selected is in foreground layer
          new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
          { locationSpot: go.Spot.Center },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          new go.Binding("text", "key"),
          { // what to do when a drag-over or a drag-drop occurs on a Node representing a table
            mouseDragEnter: function(e, node, prev) {
              var dragCopy = node.diagram.toolManager.draggingTool.copiedParts;  // could be copied from palette
              highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, true);
            },
            mouseDragLeave: function(e, node, next) {
              var dragCopy = node.diagram.toolManager.draggingTool.copiedParts;
              highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, false);
            },
            mouseDrop: function(e, node) {
              assignPeopleToSeats(node, node.diagram.selection, e.documentPoint);
            }
          },
          $(go.Shape, "Rectangle", { fill: "blanchedalmond", stroke: null }),
          $(go.Panel, "Viewbox",
            { desiredSize: new go.Size(50, 38) },
            $(go.TextBlock, { margin: 2, desiredSize: new go.Size(55, NaN), font: "8pt Verdana, sans-serif", textAlign: "center", stroke: "darkblue" },
              new go.Binding("text", "", function(data) {
                var s = data.key;
                if (data.plus) s += " +" + data.plus.toString();
                return s;
              }))
          )
        ));

      // Create a seat element at a particular alignment relative to the table.
      function Seat(number, align, focus) {
        if (typeof align === 'string') align = go.Spot.parse(align);
        if (!align || !align.isSpot()) align = go.Spot.Right;
        if (typeof focus === 'string') focus = go.Spot.parse(focus);
        if (!focus || !focus.isSpot()) focus = align.opposite();
        return $(go.Panel, "Spot",
          { name: number.toString(), alignment: align, alignmentFocus: focus },
          $(go.Shape, "Circle",
            { name: "SEATSHAPE", desiredSize: new go.Size(40, 40), fill: "burlywood", stroke: "white", strokeWidth: 2 },
            new go.Binding("fill")),
          $(go.TextBlock, number.toString(),
            { font: "10pt Verdana, sans-serif" },
            new go.Binding("angle", "angle", function(n) { return -n; }))
        );
      }

      function tableStyle() {
        return [
          { background: "transparent" },
          { layerName: "Background" },  // behind all Persons
          { locationSpot: go.Spot.Center, locationObjectName: "TABLESHAPE" },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          { rotatable: true },
          new go.Binding("angle").makeTwoWay(),
          { // what to do when a drag-over or a drag-drop occurs on a Node representing a table
            mouseDragEnter: function(e, node, prev) {
              var dragCopy = node.diagram.toolManager.draggingTool.copiedParts;  // could be copied from palette
              highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, true);
            },
            mouseDragLeave: function(e, node, next) {
              var dragCopy = node.diagram.toolManager.draggingTool.copiedParts;
              highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, false);
            },
            mouseDrop: function(e, node) { assignPeopleToSeats(node, node.diagram.selection, e.documentPoint); }
          }
        ];
      }

      // various kinds of tables:

      myDiagram.nodeTemplateMap.add("TableR8",  // rectangular with 8 seats
        $(go.Node, "Spot", tableStyle(),
          $(go.Panel, "Spot",
            $(go.Shape, "Rectangle",
              { name: "TABLESHAPE", desiredSize: new go.Size(160, 80), fill: "burlywood", stroke: null },
              new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
              new go.Binding("fill")),
            $(go.TextBlock, { editable: true, font: "bold 11pt Verdana, sans-serif" },
              new go.Binding("text", "name").makeTwoWay(),
              new go.Binding("angle", "angle", function(n) { return -n; }))
          ),
          Seat(1, "0.2 0", "0.5 1"),
          Seat(2, "0.5 0", "0.5 1"),
          Seat(3, "0.8 0", "0.5 1"),
          Seat(4, "1 0.5", "0 0.5"),
          Seat(5, "0.8 1", "0.5 0"),
          Seat(6, "0.5 1", "0.5 0"),
          Seat(7, "0.2 1", "0.5 0"),
          Seat(8, "0 0.5", "1 0.5")
        ));

      myDiagram.nodeTemplateMap.add("TableR3",  // rectangular with 3 seats in a line
        $(go.Node, "Spot", tableStyle(),
          $(go.Panel, "Spot",
            $(go.Shape, "Rectangle",
              { name: "TABLESHAPE", desiredSize: new go.Size(160, 60), fill: "burlywood", stroke: null },
              new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
              new go.Binding("fill")),
            $(go.TextBlock, { editable: true, font: "bold 11pt Verdana, sans-serif" },
              new go.Binding("text", "name").makeTwoWay(),
              new go.Binding("angle", "angle", function(n) { return -n; }))
          ),
          Seat(1, "0.2 0", "0.5 1"),
          Seat(2, "0.5 0", "0.5 1"),
          Seat(3, "0.8 0", "0.5 1")
        ));

      myDiagram.nodeTemplateMap.add("TableC8",  // circular with 8 seats
        $(go.Node, "Spot", tableStyle(),
          $(go.Panel, "Spot",
            $(go.Shape, "Circle",
              { name: "TABLESHAPE", desiredSize: new go.Size(120, 120), fill: "burlywood", stroke: null },
              new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
              new go.Binding("fill")),
            $(go.TextBlock, { editable: true, font: "bold 11pt Verdana, sans-serif" },
              new go.Binding("text", "name").makeTwoWay(),
              new go.Binding("angle", "angle", function(n) { return -n; }))
          ),
          Seat(1, "0.50 0", "0.5 1"),
          Seat(2, "0.85 0.15", "0.15 0.85"),
          Seat(3, "1 0.5", "0 0.5"),
          Seat(4, "0.85 0.85", "0.15 0.15"),
          Seat(5, "0.50 1", "0.5 0"),
          Seat(6, "0.15 0.85", "0.85 0.15"),
          Seat(7, "0 0.5", "1 0.5"),
          Seat(8, "0.15 0.15", "0.85 0.85")
        ));

      // what to do when a drag-drop occurs in the Diagram's background
      myDiagram.mouseDrop = function(e) {
        // when the selection is dropped in the diagram's background,
        // make sure the selected people no longer belong to any table
        e.diagram.selection.each(function(n) {
          if (isPerson(n)) unassignSeat(n.data);
        });
      };

      // to simulate a "move" from the Palette, the source Node must be deleted.
      myDiagram.addDiagramListener("ExternalObjectsDropped", function(e) {
        // if any Tables were dropped, don't delete from myGuests
        if (!e.subject.any(isTable)) {
          myGuests.commandHandler.deleteSelection();
        }
      });

      // put deleted people back in the myGuests diagram
      myDiagram.addDiagramListener("SelectionDeleted", function(e) {
        // no-op if deleted by myGuests' ExternalObjectsDropped listener
        if (myDiagram.disableSelectionDeleted) return;
        // e.subject is the myDiagram.selection collection
        e.subject.each(function(n) {
          if (isPerson(n)) {
            myGuests.model.addNodeData(myGuests.model.copyNodeData(n.data));
          }
        });
      });

      // create some initial tables
      myDiagram.model = new go.GraphLinksModel([
        { "key": 1, "category": "TableR3", "name": "Head 1", "guests": {}, "loc": "143.5 58" },
        { "key": 2, "category": "TableR3", "name": "Head 2", "guests": {}, "loc": "324.5 58" },
        { "key": 3, "category": "TableR8", "name": "3", "guests": {}, "loc": "121.5 203.5" },
        { "key": 4, "category": "TableC8", "name": "4", "guests": {}, "loc": "364.5 223.5" }
      ]);  // this sample does not make use of any links


      // initialize the Palette
      myGuests =
        $(go.Palette, "myGuests",
          {
            nodeTemplateMap: myDiagram.nodeTemplateMap,
            layout: $(go.GridLayout,
              {
                sorting: go.GridLayout.Ascending  // sort by Node.text value
              })
          });

      // specify the contents of the Palette
      myGuests.model = new go.GraphLinksModel([
        { key: "Tyrion Lannister" },
        { key: "Daenerys Targaryen", plus: 3 },  // dragons, of course
        { key: "Jon Snow" },
        { key: "Stannis Baratheon" },
        { key: "Arya Stark" },
        { key: "Jorah Mormont" },
        { key: "Sandor Clegane" },
        { key: "Joffrey Baratheon" },
        { key: "Brienne of Tarth" },
        { key: "Hodor" }
      ]);


      // initialize the Tables Palette
      myTables =
        $(go.Palette, "myTables",
          {
            initialScale: 0.5,
            nodeTemplateMap: myDiagram.nodeTemplateMap,
            layout: $(go.GridLayout,
              {
                spacing: new go.Size(40, 40),
                sorting: go.GridLayout.Ascending  // sort by Node.text value
              })
          });
      
      myTables.model = new go.GraphLinksModel([
        { "category": "TableR3", "name": "Rect 3", "guests": {} },
        { "category": "TableR8", "name": "Rect 8", "guests": {} },
        { "category": "TableC8", "name": "Circle 8", "guests": {} }
      ]);
    } // end init

    function isPerson(n) { return n !== null && n.category === ""; }

    function isTable(n) { return n !== null && n.category !== ""; }

    // Highlight the empty and occupied seats at a "Table" Node
    function highlightSeats(node, coll, show) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      var it = coll.iterator;
      while (it.next()) {
        var n = it.key;
        // if dragging a Table, don't do any highlighting
        if (isTable(n)) return;
      }
      var guests = node.data.guests;
      for (var sit = node.elements; sit.next();) {
        var seat = sit.value;
        if (seat.name) {
          var num = parseFloat(seat.name);
          if (isNaN(num)) continue;
          var seatshape = seat.findObject("SEATSHAPE");
          if (!seatshape) continue;
          if (show) {
            if (guests[seat.name]) {
              seatshape.stroke = "red";
            } else {
              seatshape.stroke = "green";
            }
          } else {
            seatshape.stroke = "white";
          }
        }
      }
    }

    // Given a "Table" Node, assign seats for all of the people in the given collection of Nodes;
    // the optional Point argument indicates where the collection of people may have been dropped.
    function assignPeopleToSeats(node, coll, pt) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      if (coll.any(isTable)) {
        // if dragging a Table, don't allow it to be dropped onto another table
        myDiagram.currentTool.doCancel();
        return;
      }
      // OK -- all Nodes are people, call assignSeat on each person data
      coll.each(function(n) { assignSeat(node, n.data, pt); });
      positionPeopleAtSeats(node);
    }

    // Given a "Table" Node, assign one guest data to a seat at that table.
    // Also handles cases where the guest represents multiple people, because guest.plus > 0.
    // This tries to assign the unoccupied seat that is closest to the given point in document coordinates.
    function assignSeat(node, guest, pt) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      if (guest instanceof go.GraphObject) throw Error("A guest object must not be a GraphObject: " + guest.toString());
      if (!(pt instanceof go.Point)) pt = node.location;

      // in case the guest used to be assigned to a different seat, perhaps at a different table
      unassignSeat(guest);

      var model = node.diagram.model;
      var guests = node.data.guests;
      // iterate over all seats in the Node to find one that is not occupied
      var closestseatname = findClosestUnoccupiedSeat(node, pt);
      if (closestseatname) {
        model.setDataProperty(guests, closestseatname, guest.key);
        model.setDataProperty(guest, "table", node.data.key);
        model.setDataProperty(guest, "seat", parseFloat(closestseatname));
      }

      var plus = guest.plus;
      if (plus) {  // represents several people
        // forget the "plus" info, since next we create N copies of the node/data
        guest.plus = undefined;
        model.updateTargetBindings(guest);
        for (var i = 0; i < plus; i++) {
          var copy = model.copyNodeData(guest);
          // don't copy the seat assignment of the first person
          copy.table = undefined;
          copy.seat = undefined;
          model.addNodeData(copy);
          assignSeat(node, copy, pt);
        }
      }
    }

    // Declare that the guest represented by the data is no longer assigned to a seat at a table.
    // If the guest had been at a table, the guest is removed from the table's list of guests.
    function unassignSeat(guest) {
      if (guest instanceof go.GraphObject) throw Error("A guest object must not be a GraphObject: " + guest.toString());
      var model = myDiagram.model;
      // remove from any table that the guest is assigned to
      if (guest.table) {
        var table = model.findNodeDataForKey(guest.table);
        if (table) {
          var guests = table.guests;
          if (guests) model.setDataProperty(guests, guest.seat.toString(), undefined);
        }
      }
      model.setDataProperty(guest, "table", undefined);
      model.setDataProperty(guest, "seat", undefined);
    }

    // Find the name of the unoccupied seat that is closest to the given Point.
    // This returns null if no seat is available at this table.
    function findClosestUnoccupiedSeat(node, pt) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      var guests = node.data.guests;
      var closestseatname = null;
      var closestseatdist = Infinity;
      // iterate over all seats in the Node to find one that is not occupied
      for (var sit = node.elements; sit.next();) {
        var seat = sit.value;
        if (seat.name) {
          var num = parseFloat(seat.name);
          if (isNaN(num)) continue;  // not really a "seat"
          if (guests[seat.name]) continue;  // already assigned
          var seatloc = seat.getDocumentPoint(go.Spot.Center);
          var seatdist = seatloc.distanceSquaredPoint(pt);
          if (seatdist < closestseatdist) {
            closestseatdist = seatdist;
            closestseatname = seat.name;
          }
        }
      }
      return closestseatname;
    }

    // Position the nodes of all of the guests that are seated at this table
    // to be at their corresponding seat elements of the given "Table" Node.
    function positionPeopleAtSeats(node) {
      if (isPerson(node)) {  // refer to the person's table instead
        node = node.diagram.findNodeForKey(node.data.table);
        if (node === null) return;
      }
      var guests = node.data.guests;
      var model = node.diagram.model;
      for (var seatname in guests) {
        var guestkey = guests[seatname];
        var guestdata = model.findNodeDataForKey(guestkey);
        positionPersonAtSeat(guestdata);
      }
    }

    // Position a single guest Node to be at the location of the seat to which they are assigned.
    function positionPersonAtSeat(guest) {
      if (guest instanceof go.GraphObject) throw Error("A guest object must not be a GraphObject: " + guest.toString());
      if (!guest || !guest.table || !guest.seat) return;
      var diagram = myDiagram;
      var table = diagram.findPartForKey(guest.table);
      var person = diagram.findPartForData(guest);
      if (table && person) {
        var seat = table.findObject(guest.seat.toString());
        var loc = seat.getDocumentPoint(go.Spot.Center);
        person.location = loc;
      }
    }


    // Automatically drag people Nodes along with the table Node at which they are seated.
    function SpecialDraggingTool() {
      go.DraggingTool.call(this);
      this.isCopyEnabled = false;  // don't want to copy people except between Diagrams
    }
    go.Diagram.inherit(SpecialDraggingTool, go.DraggingTool);

    SpecialDraggingTool.prototype.computeEffectiveCollection = function(parts) {
      var map = go.DraggingTool.prototype.computeEffectiveCollection.call(this, parts);
      // for each Node representing a table, also drag all of the people seated at that table
      parts.each(function(table) {
        if (isPerson(table)) return;  // ignore persons
        // this is a table Node, find all people Nodes using the same table key
        for (var nit = table.diagram.nodes; nit.next();) {
          var n = nit.value;
          if (isPerson(n) && n.data.table === table.data.key) {
            if (!map.has(n)) map.add(n, new go.DraggingInfo(n.location.copy()));
          }
        }
      });
      return map;
    };
    // end SpecialDraggingTool


    // Automatically move seated people as a table is rotated, to keep them in their seats.
    // Note that because people are separate Nodes, rotating a table Node means the people Nodes
    // are not rotated, so their names (TextBlocks) remain horizontal.
    function HorizontalTextRotatingTool() {
      go.RotatingTool.call(this);
    }
    go.Diagram.inherit(HorizontalTextRotatingTool, go.RotatingTool);

    HorizontalTextRotatingTool.prototype.rotate = function(newangle) {
      go.RotatingTool.prototype.rotate.call(this, newangle);
      var node = this.adornedObject.part;
      positionPeopleAtSeats(node);
    };
    // end HorizontalTextRotatingTool
  </script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myFlexDiv">
    <div id="myGuests" style="border: solid 1px black"></div>
    <div id="myDiagramDiv" style="border: solid 1px black"></div>
    <div id="myTables" style="border: solid 1px black"></div>
  </div>
  <div>
    Diagram Model saved in JSON format, automatically updated after each transaction:
    <pre id="savedModel" style="height:250px"></pre>
  </div>
</div>
</body>
</html>

Note that by using a Palette instead of a regular Diagram, by default the Palette is read-only. In the unmodified sample, it uses a Diagram to allow it to be modified both directly and by drag-and-drop from the main Diagram.