GoJS组织结构图

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Org Chart Editor</title>
  <meta name="description" content="组织结构图编辑器-编辑详细信息并更改关系。" />
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="https://unpkg.com/gojs/release/go-debug.js"></script>
  <link rel="stylesheet" href="../extensions/dataInspector.css" />

  <script id="code">
    // 初始化JSON格式的数据
    var modelObj = {
      "class": "go.TreeModel",
      "nodeDataArray": [{
          "key": 1,
          "name": "Stella Payne Diaz",
          "title": "CEO"
        },
        {
          "key": 2,
          "name": "Luke Warm",
          "title": "VP Marketing/Sales",
          "parent": 1
        },
        {
          "key": 3,
          "name": "Meg Meehan Hoffa",
          "title": "Sales",
          "parent": 2
        },
        {
          "key": 4,
          "name": "Peggy Flaming",
          "title": "VP Engineering",
          "parent": 1
        },
        {
          "key": 5,
          "name": "Saul Wellingood",
          "title": "Manufacturing",
          "parent": 4
        },
        {
          "key": 6,
          "name": "Al Ligori",
          "title": "Marketing",
          "parent": 2
        },
        {
          "key": 7,
          "name": "Dot Stubadd",
          "title": "Sales Rep",
          "parent": 3
        },
        {
          "key": 8,
          "name": "Les Ismore",
          "title": "Project Mgr",
          "parent": 5
        },
        {
          "key": 9,
          "name": "April Lynn Parris",
          "title": "Events Mgr",
          "parent": 6
        },
        {
          "key": 10,
          "name": "Xavier Breath",
          "title": "Engineering",
          "parent": 4
        },
        {
          "key": 11,
          "name": "Anita Hammer",
          "title": "Process",
          "parent": 5
        },
        {
          "key": 12,
          "name": "Billy Aiken",
          "title": "Software",
          "parent": 10
        },
        {
          "key": 13,
          "name": "Stan Wellback",
          "title": "Testing",
          "parent": 10
        },
        {
          "key": 14,
          "name": "Marge Innovera",
          "title": "Hardware",
          "parent": 10
        },
        {
          "key": 15,
          "name": "Evan Elpus",
          "title": "Quality",
          "parent": 5
        },
        {
          "key": 16,
          "name": "Lotta B. Essen",
          "title": "Sales Rep",
          "parent": 3
        }
      ]
    };

    function init() {
      if (window.goSamples) goSamples(); // 这些样本的初始化-您无需调用
      var $ = go.GraphObject.make; // 为了简洁定义模板

      myDiagram =
        $(go.Diagram, "myDiagramDiv", // 必须是div的ID或引用。个DIV必须指定宽高,否者不会被渲染出来,我们通常为DIV设置一个背景颜色以便于我们更便捷的观察
          {
            maxSelectionCount: 1, // 用户一次只能选择一个零件
            "undoManager.isEnabled": false, //  支持 Ctrl-Z 和 Ctrl-Y 操作
            //禁止横竖拖动和鼠标滚动,流程图整体的拖动
            "allowHorizontalScroll": false,
            "allowVerticalScroll": false,
            "isReadOnly": true, // 只读,可以禁止拖动
            validCycle: go.Diagram.CycleDestinationTree, // 确保用户只能创建树
            "clickCreatingTool.archetypeNodeData": { // 允许在后台双击以创建新节点
              name: "(new person)",
              title: "",
              comments: ""
            },
            "clickCreatingTool.insertPart": function (loc) { // 滚动到新节点
              var node = go.ClickCreatingTool.prototype.insertPart.call(this, loc);
              if (node !== null) {
                this.diagram.select(node);
                this.diagram.commandHandler.scrollToPart(node);
                this.diagram.commandHandler.editTextBlock(node.findObject("NAMETB"));
              }
              return node;
            },
            layout: $(go.TreeLayout, {
              treeStyle: go.TreeLayout.StyleLastParents,
              arrangement: go.TreeLayout.ArrangementHorizontal,
              // 大多数树的属性:
              angle: 90,
              layerSpacing: 35,
              // “最后父母”的属性:
              alternateAngle: 90,
              alternateLayerSpacing: 35,
              alternateAlignment: go.TreeLayout.AlignmentBus,
              alternateNodeSpacing: 20
            }),
            "undoManager.isEnabled": true // 启用撤消和重做
          });

      // 修改文档后,在标题上添加“*”并启用“保存”按钮
      myDiagram.addDiagramListener("Modified", function (e) {
        var button = document.getElementById("SaveButton");
        if (button) button.disabled = !myDiagram.isModified;
        var idx = document.title.indexOf("*");
        if (myDiagram.isModified) {
          if (idx < 0) document.title += "*";
        } else {
          if (idx >= 0) document.title = document.title.substr(0, idx);
        }
      });

      // 从图中删除节点或链接时,手动管理老板信息
      myDiagram.addDiagramListener("SelectionDeleting", function (e) {
        var part = e.subject.first(); // e.subject是myDiagram.selection集合,
        // 所以我们将获得第一个,因为我们知道我们只有一个选择
        myDiagram.startTransaction("clear boss");
        if (part instanceof go.Node) {
          var it = part.findTreeChildrenNodes(); // 查找所有子节点
          while (it.next()) { // 现在遍历他们并清除老板信息
            var child = it.value;
            var bossText = child.findObject("boss"); // 由于老板TextBlock是命名的,我们可以按名称访问它
            if (bossText === null) return;
            bossText.text = "";
          }
        } else if (part instanceof go.Link) {
          var child = part.toNode;
          var bossText = child.findObject("boss"); // 由于老板TextBlock是命名的,我们可以按名称访问它
          if (bossText === null) return;
          bossText.text = "";
        }
        myDiagram.commitTransaction("clear boss");
      });

      var levelColors = ["#AC193D", "#2672EC", "#8C0095", "#5133AB",
        "#008299", "#D24726", "#008A00", "#094AB2"
      ];

      // 重写TreeLayout.commitNodes还可基于树的深度级别修改背景画笔
      myDiagram.layout.commitNodes = function () {
        go.TreeLayout.prototype.commitNodes.call(myDiagram.layout); // 标准行为
        // 然后遍历所有顶点并设置其对应的节点Shape.fill
        // 依赖于TreeVertex.level值的画笔
        myDiagram.layout.network.vertexes.each(function (v) {
          if (v.node) {
            var level = v.level % (levelColors.length);
            var color = levelColors[level];
            var shape = v.node.findObject("SHAPE");
            if (shape) shape.stroke = $(go.Brush, "Linear", {
              0: color,
              1: go.Brush.lightenBy(color, 0.05),
              start: go.Spot.Left,
              end: go.Spot.Right
            });
          }
        });
      };

      // 双击节点时,向其添加一个子节点
      function nodeDoubleClick(e, obj) {
        var clicked = obj.part;
        if (clicked !== null) {
          var thisemp = clicked.data;
          myDiagram.startTransaction("add employee");
          var newemp = {
            name: "(new person)",
            title: "",
            comments: "",
            parent: thisemp.key
          };
          myDiagram.model.addNodeData(newemp);
          myDiagram.commitTransaction("add employee");
        }
      }

      // 单击节点时,向其添加一个子节点
      function nodeClick(e, obj) {
        var clicked = obj.part;
        if (clicked !== null) {
          var thisemp = clicked.data;
          myDiagram.startTransaction("add employee");
          var newemp = {
            name: "(new person)",
            title: "",
            comments: "",
            parent: thisemp.key
          };
          myDiagram.model.addNodeData(newemp);
          myDiagram.commitTransaction("add employee");
        }
      }

      // 用于确定拖动过程中的反馈
      function mayWorkFor(node1, node2) {
        if (!(node1 instanceof go.Node)) return false; // 必须是一个节点
        if (node1 === node2) return false; // 不能为自己工作
        if (node2.isInTreeOf(node1)) return false; // 不能为为你工作的人工作
        return true;
      }

      // 此功能为大多数TextBlock提供了通用样式。
      // 其中某些值可能在特定的TextBlock中被覆盖。
      function textStyle() {
        return {
          font: "9pt  Segoe UI,sans-serif",
          stroke: "white"
        };
      }

      // 图片使用了此转换器。
      function findHeadShot(key) {
        if (key < 0 || key > 16) return "D:/GoJS/site/samples/images/HS16.png"; // 服务器上只有16张图像
        return "D:/GoJS/site/samples/images/HS" + key + ".jpg"
      }

      // 定义节点模板
      myDiagram.nodeTemplate =
        $(go.Node, "Auto",
          /*{
                     //doubleClick: nodeDoubleClick 
                     //单击节点,通知外部打开编辑浮窗,外部点击保存,调用内部的保存方法,外部关闭,内部不保存
                     click: nodeClick
                   }, */
          { // 处理将一个节点拖到一个节点上以(可能)更改报告关系
            mouseDragEnter: function (e, node, prev) {
              var diagram = node.diagram;
              var selnode = diagram.selection.first();
              if (!mayWorkFor(selnode, node)) return;
              var shape = node.findObject("SHAPE");
              if (shape) {
                shape._prevFill = shape.fill; // 记得原来的刷子
                shape.fill = "darkred";
              }
            },
            mouseDragLeave: function (e, node, next) {
              var shape = node.findObject("SHAPE");
              if (shape && shape._prevFill) {
                shape.fill = shape._prevFill; // 恢复原始画笔
              }
            },
            mouseDrop: function (e, node) {
              var diagram = node.diagram;
              var selnode = diagram.selection.first(); // 假设选择中只有一个节点
              if (mayWorkFor(selnode, node)) {
                // 查找到所选节点的任何现有链接
                var link = selnode.findTreeParentLink();
                if (link !== null) { // 重新连接任何现有链接
                  link.fromNode = node;
                } else { // 否则创建一个新链接
                  diagram.toolManager.linkingTool.insertLink(node, node.port, selnode, selnode.port);
                }
              }
            }
          },
          // 为了排序,让Node.text为data.name
          new go.Binding("text", "name"),
          // 绑定Part.layerName来控制节点的图层,具体取决于是否被选中
          new go.Binding("layerName", "isSelected", function (sel) {
            return sel ? "Foreground" : "";
          }).ofObject(),
          // define the node's outer shape
          $(go.Shape, "Rectangle", {
            name: "SHAPE",
            fill: "#333333",
            stroke: 'white',
            strokeWidth: 3.5,
            // 设置端口属性:
            portId: "",
            fromLinkable: true,
            toLinkable: true,
            cursor: "pointer"
          }),
          $(go.Panel, "Horizontal",
            $(go.Picture, {
                name: "Picture",
                desiredSize: new go.Size(70, 70),
                margin: 1.5,
              },
              new go.Binding("source", "key", findHeadShot)),
            // 定义显示文本的面板
            $(go.Panel, "Table", {
                minSize: new go.Size(130, NaN),
                maxSize: new go.Size(150, NaN),
                margin: new go.Margin(6, 10, 0, 6),
                defaultAlignment: go.Spot.Left
              },
              $(go.RowColumnDefinition, {
                column: 2,
                 4
              }),
              $(go.TextBlock, textStyle(), // 名字
                {
                  row: 0,
                  column: 0,
                  columnSpan: 5,
                  font: "12pt Segoe UI,sans-serif",
                  editable: true,
                  isMultiline: false,
                  minSize: new go.Size(10, 16)
                },
                new go.Binding("text", "name").makeTwoWay()),
              $(go.TextBlock, "Title: ", textStyle(), {
                row: 1,
                column: 0
              }),
              $(go.TextBlock, textStyle(), {
                  row: 1,
                  column: 1,
                  columnSpan: 4,
                  editable: true,
                  isMultiline: false,
                  minSize: new go.Size(10, 14),
                  margin: new go.Margin(0, 0, 0, 3)
                },
                new go.Binding("text", "title").makeTwoWay()),
              $(go.TextBlock, textStyle(), {
                  row: 2,
                  column: 0
                },
                new go.Binding("text", "key", function (v) {
                  return "ID: " + v;
                })),
              $(go.TextBlock, textStyle(), {
                  name: "boss",
                  row: 2,
                  column: 3,
                }, // 我们包含一个名称,以便我们在删除节点/链接时可以访问此TextBlock
                new go.Binding("text", "parent", function (v) {
                  return "Boss: " + v;
                })),
              $(go.TextBlock, textStyle(), // 评论
                {
                  row: 3,
                  column: 0,
                  columnSpan: 5,
                  font: "italic 9pt sans-serif",
                  wrap: go.TextBlock.WrapFit,
                  editable: true, // 默认情况下允许换行
                  minSize: new go.Size(10, 14)
                },
                new go.Binding("text", "comments").makeTwoWay())
            ) // end Table Panel
          ) // end Horizontal Panel
        ); // end Node
      // 选中的节点显示用于添加子节点的按钮
      myDiagram.nodeTemplate.selectionAdornmentTemplate =
        $(go.Adornment, "Spot",
          $(go.Panel, "Auto",
            $(go.Placeholder, {
              margin: new go.Margin(4, -20, 50, 4)
            })
          ),
          // 所选节点的删除按钮
          $("Button", {
              alignment: go.Spot.Right,
              alignmentFocus: go.Spot.Left,
              click: function (e, obj) {
                // 删除整个子树,包括节点本身
                var node = obj.part.adornedPart;
                if (node !== null) {
                  myDiagram.startTransaction("remove dept");
                  myDiagram.removeParts(node.findTreeParts());
                  myDiagram.commitTransaction("remove dept");
                }
              }
              // 定义装饰中此按钮的单击行为
            },
            $(go.TextBlock, "-", // 按钮的内容
              {
                font: "bold 8pt sans-serif"
              })
          ),
          // 所选节点的新增按钮
          $("Button", {
              alignment: go.Spot.Right,
              alignmentFocus: go.Spot.Right,
              click: nodeClick
            },
            $(go.TextBlock, "+", // 按钮的内容
              {
                font: "bold 8pt sans-serif"
              })
          )
        );
      // 上下文菜单允许用户腾空职位,
      // 删除角色并重新分配子树,或删除部门
      myDiagram.nodeTemplate.contextMenu =
        $("ContextMenu",
          $("ContextMenuButton",
            $(go.TextBlock, "Vacate Position"), {
              click: function (e, obj) {
                var node = obj.part.adornedPart;
                if (node !== null) {
                  var thisemp = node.data;
                  myDiagram.startTransaction("vacate");
                  // 更新密钥,名称和注释
                  myDiagram.model.setDataProperty(thisemp, "name", "(Vacant)");
                  myDiagram.model.setDataProperty(thisemp, "comments", "");
                  myDiagram.commitTransaction("vacate");
                }
              }
            }
          ),
          $("ContextMenuButton",
            $(go.TextBlock, "Remove Role"), {
              click: function (e, obj) {
                // 将子树重新关联到该节点的老板,然后删除该节点
                var node = obj.part.adornedPart;
                if (node !== null) {
                  myDiagram.startTransaction("reparent remove");
                  var chl = node.findTreeChildrenNodes();
                  // 遍历子节点并将其父密钥设置为我们所选节点的父密钥
                  while (chl.next()) {
                    var emp = chl.value;
                    myDiagram.model.setParentKeyForNodeData(emp.data, node.findTreeParentNode().data.key);
                  }
                  // 现在删除所选节点本身
                  myDiagram.model.removeNodeData(node.data);
                  myDiagram.commitTransaction("reparent remove");
                }
              }
            }
          ),
          $("ContextMenuButton",
            $(go.TextBlock, "Remove Department"), {
              click: function (e, obj) {
                // 删除整个子树,包括节点本身
                var node = obj.part.adornedPart;
                if (node !== null) {
                  myDiagram.startTransaction("remove dept");
                  myDiagram.removeParts(node.findTreeParts());
                  myDiagram.commitTransaction("remove dept");
                }
              }
            }
          )
        );

      // 定义链接模板
      myDiagram.linkTemplate =
        $(go.Link, go.Link.Orthogonal, {
            corner: 5,
            relinkableFrom: true,
            relinkableTo: true
          },
          $(go.Shape, {
            strokeWidth: 1.5,
            stroke: "#F5F5F5"
          })); // the link shape

      //从modelObj读取JSON格式的数据
      load();

      // 设置缩放以适合按钮
      document.getElementById('zoomToFit').addEventListener('click', function () {
        myDiagram.commandHandler.zoomToFit();
      });

      document.getElementById('centerRoot').addEventListener('click', function () {
        myDiagram.scale = 1;
        myDiagram.commandHandler.scrollToPart(myDiagram.findNodeForKey(1));
      });

      // 支持在HTML中编辑所选人员的属性
      if (window.Inspector) myInspector = new Inspector("myInspector", myDiagram, {
        properties: {
          "key": {
            readOnly: true
          },
          "comments": {}
        }
      });

    } // 初始化结束

    // 以JSON格式显示图的模型
    function save() {
      modelObj = myDiagram.model.toJson();
      myDiagram.isModified = false;
    }

    function load() {
      myDiagram.model = go.Model.fromJson(modelObj);
      // 确保新数据键是唯一的正整数
      var lastkey = 1;
      myDiagram.model.makeUniqueKeyFunction = function (model, data) {
        var k = data.key || lastkey;
        while (model.findNodeDataForKey(k)) k++;
        data.key = lastkey = k;
        return k;
      };
    }

    //----------------------------../extensions/dataInspector.js--------
    "use strict";
    /**
      此类为GoJS模型数据对象实现检查器。
      构造函数采用三个参数:
        {string}分隔一个字符串,该字符串引用要检查的div的HTML ID。
        {Diagram}图是对GoJS图的引用。
        {Object} options可选的JS Object,描述检查器的选项。

      选项:
        inspectSelection {boolean}默认为true,是否自动显示和填充Inspector
                                   与当前选择的图零件。如果设置为false,则检查器将不显示任何内容
                                   直到您以Part或JavaScript对象作为参数调用Inspector.inspectObject(object)。
        includesOwnProperties {boolean}默认为true,是否列出当前在检查的数据对象上的所有属性。
        properties {Object}一个字符串对象:对象对,代表propertyName:propertyOptions。
                            可用于包括或排除其他属性。
        propertyModified函数(propertyName,newValue)回调
        multipleSelection {boolean}默认为false,是否允许多重选择并更改所有选定属性而不是
                                    单个第一个对象
        showAllProperties {boolean}默认为false,与multiSelection一起显示的属性在false时使用属性的相交还是在true时使用并集
                                    仅在multipleSelection为true时影响
        showSize {number}默认值0,显示选择多个节点时显示多少个节点
                          当其小于1时,显示所有节点

      属性选项:
        show:{boolean | function}一个布尔值,以向检查器显示或隐藏该属性,或者一个谓词函数以有条件地显示。
        readOnly:{boolean | function}该属性是否为只读
        类型:{string}描述数据类型的字符串。支持的值:“字符串|数字|布尔值|颜色|数字数组|点|矩形|大小|位置|边距|选择”
        defaultValue:{*}属性的默认值。默认为空字符串。
        选择:{Array | function}当type ==“ select”时,要使用的选择数组或返回选择数组的函数。

      Inspector的用法示例:

      var inspector = new Inspector("myInspector", myDiagram,
        {
          includesOwnProperties: false,
          properties: {
            "key": { show: Inspector.showIfPresent, readOnly: true },
            "comments": { show: Inspector.showIfNode  },
            "LinkComments": { show: Inspector.showIfLink },
            "chosen": { show: Inspector.showIfNode, type: "checkbox" },
            "state": { show: Inspector.showIfNode, type: "select", choices: ["Stopped", "Parked", "Moving"] }
          }
        });

      这是检查器在给定的DIV元素内创建的基本HTML结构:

      <div id="divid" class="inspector">
        <tr>
          <td>propertyName</td>
          <td><input value=propertyValue /></td>
        </tr>
        ...
      </div>

    */
    function Inspector(divid, diagram, options) {
      var mainDiv = document.getElementById(divid);
      mainDiv.className = "inspector";
      mainDiv.innerHTML = "";
      this._div = mainDiv;
      this._diagram = diagram;
      this._inspectedProperties = {};
      this._multipleProperties = {};

      // GoJS Part或简单的数据对象,例如Model.modelData
      this.inspectedObject = null;

      // 督察选项默认值:
      this.includesOwnProperties = true;
      this.declaredProperties = {};
      this.inspectsSelection = true;
      this.propertyModified = null;
      this.multipleSelection = false;
      this.showAllProperties = false;
      this.showSize = 0;

      if (options !== undefined) {
        if (options["includesOwnProperties"] !== undefined) this.includesOwnProperties = options[
          "includesOwnProperties"];
        if (options["properties"] !== undefined) this.declaredProperties = options["properties"];
        if (options["inspectSelection"] !== undefined) this.inspectsSelection = options["inspectSelection"];
        if (options["propertyModified"] !== undefined) this.propertyModified = options["propertyModified"];
        if (options['multipleSelection'] !== undefined) this.multipleSelection = options['multipleSelection'];
        if (options['showAllProperties'] !== undefined) this.showAllProperties = options['showAllProperties'];
        if (options['showSize'] !== undefined) this.showSize = options['showSize'];
      }

      var self = this;
      diagram.addModelChangedListener(function (e) {
        if (e.isTransactionFinished) self.inspectObject();
      });
      if (this.inspectsSelection) {
        diagram.addDiagramListener("ChangedSelection", function (e) {
          self.inspectObject();
        });
      }
    }

    // 一些与“ show”属性一起使用的静态谓词。
    Inspector.showIfNode = function (part) {
      return part instanceof go.Node
    };
    Inspector.showIfLink = function (part) {
      return part instanceof go.Link
    };
    Inspector.showIfGroup = function (part) {
      return part instanceof go.Group
    };

    // 仅显示该属性(如果存在)。 对于“键”很有用,它将显示在节点和组上,但通常不会显示在链接上
    Inspector.showIfPresent = function (data, propname) {
      if (data instanceof go.Part) data = data.data;
      return typeof data === "object" && data[propname] !== undefined;
    };

    /**
     * 给定{@link #inspectedObject}的属性,请更新此检查器的HTML状态。
     * @param {Object}对象是一个可选参数,当{@link #inspectSelection}为false时,
     * 以使用它来设置{@link #inspectedObject}并显示和编辑该对象的属性。
     */
    Inspector.prototype.inspectObject = function (object) {
      var inspectedObject = null;
      var inspectedObjects = null;
      if (object === null) return;
      if (object === undefined) {
        if (this.inspectsSelection) {
          if (this.multipleSelection) { // 如果多项选择为true,则获取选择
            inspectedObjects = this._diagram.selection;
          } else { // 否则抢第一个对象
            inspectedObject = this._diagram.selection.first();
          }
        } else { // 如果只有一个检查对象
          inspectedObject = this.inspectedObject;
        }
      } else { // 如果对象作为参数传递
        inspectedObject = object;
      }
      if (inspectedObjects && inspectedObjects.count === 1) {
        inspectedObject = inspectedObjects.first();
      }
      if (inspectedObjects && inspectedObjects.count <= 1) {
        inspectedObjects = null;
      }

      // 单个对象或没有对象
      if (!inspectedObjects || !this.multipleSelection) {
        if (inspectedObject === null) {
          this.inspectedObject = inspectedObject;
          this.updateAllHTML();
          return;
        }

        this.inspectedObject = inspectedObject;
        if (this.inspectObject === null) return;
        var mainDiv = this._div;
        mainDiv.innerHTML = '';

        // 使用Part.data或对象本身(对于model.modelData)
        var data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
        if (!data) return;
        // 构建表:
        var table = document.createElement('table');
        var tbody = document.createElement('tbody');
        this._inspectedProperties = {};
        this.tabIndex = 0;
        var declaredProperties = this.declaredProperties;

        // 仔细检查传递给检查器的所有属性,并显示它们(如果适用):
        for (var name in declaredProperties) {
          var desc = declaredProperties[name];
          if (!this.canShowProperty(name, desc, inspectedObject)) continue;
          var val = this.findValue(name, desc, data);
          tbody.appendChild(this.buildPropertyRow(name, val));
        }
        // 浏览模型数据上的所有属性,并显示它们(如果适用):
        if (this.includesOwnProperties) {
          for (var k in data) {
            if (k === '__gohashid') continue; // 跳过内部GoJS哈希属性
            if (this._inspectedProperties[k]) continue; // 已经存在
            if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject)) continue;
            tbody.appendChild(this.buildPropertyRow(k, data[k]));
          }
        }

        table.appendChild(tbody);
        mainDiv.appendChild(table);
      } else { // 选择了多个对象
        var mainDiv = this._div;
        mainDiv.innerHTML = '';
        var shared = new go.Map(); // 用于节点共有的属性
        var properties = new go.Map(); // 用于添加属性
        var all = new go.Map(); // 以后使用以防止在不需要时更改属性
        var it = inspectedObjects.iterator;
        // 构建表:
        var table = document.createElement('table');
        var tbody = document.createElement('tbody');
        this._inspectedProperties = {};
        this.tabIndex = 0;
        var declaredProperties = this.declaredProperties;
        it.next();
        inspectedObject = it.value;
        this.inspectedObject = inspectedObject;
        var data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
        if (data) { // 初始通行证以设置共享和全部
          // 仔细检查传递给检查器的所有属性,并将它们添加到地图(如果适用):
          for (var name in declaredProperties) {
            var desc = declaredProperties[name];
            if (!this.canShowProperty(name, desc, inspectedObject)) continue;
            var val = this.findValue(name, desc, data);
            if (val === '' && desc && desc.type === 'checkbox') {
              shared.add(name, false);
              all.add(name, false);
            } else {
              shared.add(name, val);
              all.add(name, val);
            }
          }
          // 浏览模型数据上的所有属性,并将它们添加到地图(如果适用):
          if (this.includesOwnProperties) {
            for (var k in data) {
              if (k === '__gohashid') continue; // 跳过内部GoJS哈希属性
              if (this._inspectedProperties[k]) continue; // 已经存在
              if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject)) continue;
              shared.add(k, data[k]);
              all.add(k, data[k]);
            }
          }
        }
        var nodecount = 2;
        while (it.next() && (this.showSize < 1 || nodecount <= this
            .showSize)) { // grabs all the properties from the other selected objects
          properties.clear();
          inspectedObject = it.value;
          if (inspectedObject) {
            // 使用Part.data或对象本身(对于model.modelData)
            data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
            if (data) {
              // 仔细检查传递给检查器的所有属性,并将它们添加到要添加的属性中(如果适用):
              for (var name in declaredProperties) {
                var desc = declaredProperties[name];
                if (!this.canShowProperty(name, desc, inspectedObject)) continue;
                var val = this.findValue(name, desc, data);
                if (val === '' && desc && desc.type === 'checkbox') {
                  properties.add(name, false);
                } else {
                  properties.add(name, val);
                }
              }
              // 遍历模型数据上的所有属性,并将它们添加到要添加的属性中(如果适用):
              if (this.includesOwnProperties) {
                for (var k in data) {
                  if (k === '__gohashid') continue; // 跳过内部GoJS哈希属性
                  if (this._inspectedProperties[k]) continue; // 已经存在
                  if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject))
                    continue;
                  properties.add(k, data[k]);
                }
              }
            }
          }
          if (!this.showAllProperties) {
            // 使用选定对象之间不共享的属性清理共享地图
            // 如果适用,还将属性添加到添加和共享地图
            var addIt = shared.iterator;
            var toRemove = [];
            while (addIt.next()) {
              if (properties.has(addIt.key)) {
                var newVal = all.get(addIt.key) + '|' + properties.get(addIt.key);
                all.set(addIt.key, newVal);
                if ((declaredProperties[addIt.key] && declaredProperties[addIt.key].type !== 'color' &&
                    declaredProperties[addIt.key].type !== 'checkbox' && declaredProperties[addIt.key].type !==
                    'select') ||
                  !declaredProperties[addIt.key]) { // 用于非字符串属性,即颜色
                  newVal = shared.get(addIt.key) + '|' + properties.get(addIt.key);
                  shared.set(addIt.key, newVal);
                }
              } else { // 由于addIct仍在迭代中删除数组
                toRemove.push(addIt.key);
              }
            }
            for (var i = 0; i < toRemove.length; i++) { // 删除任何不显示allPropertys的东西
              shared.remove(toRemove[i]);
              all.remove(toRemove[i]);
            }
          } else {
            // 使用正确数量的分隔符将缺失的属性添加到所有属性
            var addIt = properties.iterator;
            while (addIt.next()) {
              if (all.has(addIt.key)) {
                if ((declaredProperties[addIt.key] && declaredProperties[addIt.key].type !== 'color' &&
                    declaredProperties[addIt.key].type !== 'checkbox' && declaredProperties[addIt.key].type !==
                    'select') ||
                  !declaredProperties[addIt.key]) { // 用于非字符串属性,即颜色
                  var newVal = all.get(addIt.key) + '|' + properties.get(addIt.key);
                  all.set(addIt.key, newVal);
                }
              } else {
                var newVal = '';
                for (var i = 0; i < nodecount - 1; i++) newVal += '|';
                newVal += properties.get(addIt.key);
                all.set(addIt.key, newVal);
              }
            }
            // 添加条以防万一属性不完整
            addIt = all.iterator;
            while (addIt.next()) {
              if (!properties.has(addIt.key)) {
                if ((declaredProperties[addIt.key] && declaredProperties[addIt.key].type !== 'color' &&
                    declaredProperties[addIt.key].type !== 'checkbox' && declaredProperties[addIt.key].type !==
                    'select') ||
                  !declaredProperties[addIt.key]) { // 用于非字符串属性,即颜色
                  var newVal = all.get(addIt.key) + '|';
                  all.set(addIt.key, newVal);
                }
              }
            }
          }
          nodecount++;
        }
        // 构建表属性行并设置multipleProperties来帮助updateall
        var mapIt;
        if (!this.showAllProperties) mapIt = shared.iterator;
        else mapIt = all.iterator;
        while (mapIt.next()) {
          tbody.appendChild(this.buildPropertyRow(mapIt.key, mapIt.value)); // 显示允许的属性
        }
        table.appendChild(tbody);
        mainDiv.appendChild(table);
        var allIt = all.iterator;
        while (allIt.next()) {
          this._multipleProperties[allIt.key] = allIt.value; // 用于updateall以了解要更改的属性
        }
      }
    };

    /**
     * @ignore
     *如果不显示给定的属性,则该谓词应为false。
     *通常,它仅检查属性描述符上的“显示”值。
     * 默认值是true。
     * @param {string} propertyName属性名称
     * @param {Object} propertyDesc属性描述符
     * @param {Object} inspectedObject数据对象
     * @return {boolean}是否应在此检查器中显示特定属性
     */
    Inspector.prototype.canShowProperty = function (propertyName, propertyDesc, inspectedObject) {
      if (propertyDesc.show === false) return false;
      // if "show" is a predicate, make sure it passes or do not show this property
      if (typeof propertyDesc.show === "function") return propertyDesc.show(inspectedObject, propertyName);
      return true;
    }

    /**
     * @ignore
     *如果给定属性不应由用户编辑,则此谓词应为false。
     *通常,它仅检查属性描述符上的“ readOnly”值。
     * 默认值是true。
     * @param {string} propertyName属性名称
     * @param {Object} propertyDesc属性描述符
     * @param {Object} inspectedObject数据对象
     * @return {boolean}是否应在此检查器中显示特定属性
     */
    Inspector.prototype.canEditProperty = function (propertyName, propertyDesc, inspectedObject) {
      if (this._diagram.isReadOnly || this._diagram.isModelReadOnly) return false;
      // assume property values that are functions of Objects cannot be edited
      var data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
      var valtype = typeof data[propertyName];
      if (valtype === "function") return false;
      if (propertyDesc) {
        if (propertyDesc.readOnly === true) return false;
        // if "readOnly" is a predicate, make sure it passes or do not show this property
        if (typeof propertyDesc.readOnly === "function") return !propertyDesc.readOnly(inspectedObject, propertyName);
      }
      return true;
    }

    /**
     * @ignore
     * @param {any} propName
     * @param {any}属性
     * @param {any}数据
     * @return {any}
     */
    Inspector.prototype.findValue = function (propName, propDesc, data) {
      var val = '';
      if (propDesc && propDesc.defaultValue !== undefined) val = propDesc.defaultValue;
      if (data[propName] !== undefined) val = data[propName];
      if (val === undefined) return '';
      return val;
    }

    /**
     * @ignore
     * 将设置this._inspectedProperties [propertyName]并创建HTML表行:
     * <tr>
     * <td> propertyName </ td>
     * <td> <输入值= propertyValue /> </ td>
     * </ tr>
     * @param {string} propertyName属性名称
     * @param {*} propertyValue属性值
     * @返回表格行
     */
    Inspector.prototype.buildPropertyRow = function (propertyName, propertyValue) {
      var mainDiv = this._div;
      var tr = document.createElement("tr");

      var td1 = document.createElement("td");
      td1.textContent = propertyName;
      tr.appendChild(td1);

      var td2 = document.createElement("td");
      var decProp = this.declaredProperties[propertyName];
      var input = null;
      var self = this;

      function updateall() {
        self.updateAllProperties();
      }

      if (decProp && decProp.type === "select") {
        input = document.createElement("select");
        this.updateSelect(decProp, input, propertyName, propertyValue);
        input.addEventListener("change", updateall);
      } else {
        input = document.createElement("input");

        input.value = this.convertToString(propertyValue);
        if (decProp) {
          var t = decProp.type;
          if (t !== 'string' && t !== 'number' && t !== 'boolean' &&
            t !== 'arrayofnumber' && t !== 'point' && t !== 'size' &&
            t !== 'rect' && t !== 'spot' && t !== 'margin') {
            input.setAttribute("type", decProp.type);
          }
          if (decProp.type === "color") {
            if (input.type === "color") {
              input.value = this.convertToColor(propertyValue);
              // input.addEventListener("input", updateall);
              input.addEventListener("change", updateall);
            }
          }
          if (decProp.type === "checkbox") {
            input.checked = !!propertyValue;
            input.addEventListener("change", updateall);
          }
        }
        if (input.type !== "color") input.addEventListener("blur", updateall);
      }

      if (input) {
        input.tabIndex = this.tabIndex++;
        input.disabled = !this.canEditProperty(propertyName, decProp, this.inspectedObject);
        td2.appendChild(input);
      }
      tr.appendChild(td2);

      this._inspectedProperties[propertyName] = input;
      return tr;
    };

    /**
     * @ignore
     * HTML5颜色输入仅采用十六进制,
     * 此var HTML5 canvas将颜色转换为十六进制格式。
     * 会将“ rgb(255,0,0)”转换为“#FF0000”,依此类推。
     * @param {string} propertyValue
     * @return {string}
     */
    Inspector.prototype.convertToColor = function (propertyValue) {
      var ctx = document.createElement("canvas").getContext("2d");
      ctx.fillStyle = propertyValue;
      return ctx.fillStyle;
    };

    /**
     * @ignore
     * @param {string}
     * @return{Array。<number>}
     */
    Inspector.prototype.convertToArrayOfNumber = function (propertyValue) {
      if (propertyValue === "null") return null;
      var split = propertyValue.split(' ');
      var arr = [];
      for (var i = 0; i < split.length; i++) {
        var str = split[i];
        if (!str) continue;
        arr.push(parseFloat(str));
      }
      return arr;
    };

    /**
     * @ignore
     * @param {*}
     * @return {string}
     */
    Inspector.prototype.convertToString = function (x) {
      if (x === undefined) return "undefined";
      if (x === null) return "null";
      if (x instanceof go.Point) return go.Point.stringify(x);
      if (x instanceof go.Size) return go.Size.stringify(x);
      if (x instanceof go.Rect) return go.Rect.stringify(x);
      if (x instanceof go.Spot) return go.Spot.stringify(x);
      if (x instanceof go.Margin) return go.Margin.stringify(x);
      if (x instanceof go.List) return this.convertToString(x.toArray());
      if (Array.isArray(x)) {
        var str = "";
        for (var i = 0; i < x.length; i++) {
          if (i > 0) str += " ";
          var v = x[i];
          str += this.convertToString(v);
        }
        return str;
      }
      return x.toString();
    };

    /**
     * @ignore
     * 更新此检查器中的所有HTML。
     */
    Inspector.prototype.updateAllHTML = function () {
      var inspectedProps = this._inspectedProperties;
      var diagram = this._diagram;
      var isPart = this.inspectedObject instanceof go.Part;
      var data = isPart ? this.inspectedObject.data : this.inspectedObject;
      if (!data) { // clear out all of the fields
        for (var name in inspectedProps) {
          var input = inspectedProps[name];
          if (input instanceof HTMLSelectElement) {
            input.innerHTML = "";
          } else if (input.type === "color") {
            input.value = "#000000";
          } else if (input.type === "checkbox") {
            input.checked = false;
          } else {
            input.value = "";
          }

        }
      } else {
        for (var name in inspectedProps) {
          var input = inspectedProps[name];
          var propertyValue = data[name];
          if (input instanceof HTMLSelectElement) {
            var decProp = this.declaredProperties[name];
            this.updateSelect(decProp, input, name, propertyValue);
          } else if (input.type === "color") {
            input.value = this.convertToColor(propertyValue);
          } else if (input.type === "checkbox") {
            input.checked = !!propertyValue;
          } else {
            input.value = this.convertToString(propertyValue);
          }
        }
      }
    }

    /**
     * @ignore
     * 在给定propertyName的情况下,使用适当的选项列表更新HTMLSelectElement
     */
    Inspector.prototype.updateSelect = function (decProp, select, propertyName, propertyValue) {
      select.innerHTML = ""; // 清除那里的任何东西
      var choices = decProp.choices;
      if (typeof choices === "function") choices = choices(this.inspectedObject, propertyName);
      if (!Array.isArray(choices)) choices = [];
      decProp.choicesArray = choices; // 记住实际选择值的列表(不是字符串)
      for (var i = 0; i < choices.length; i++) {
        var choice = choices[i];
        var opt = document.createElement("option");
        opt.text = this.convertToString(choice);
        select.add(opt, null);
      }
      select.value = this.convertToString(propertyValue);
    }

    /**
     * @ignore
     * 据以下内容更新{@link #inspectedObject}的所有数据属性
     * HTML输入元素中保存的当前值。
     */
    Inspector.prototype.updateAllProperties = function () {
      var inspectedProps = this._inspectedProperties;
      var diagram = this._diagram;
      if (diagram.selection.count === 1 || !this.multipleSelection) { // 单对象更新
        var isPart = this.inspectedObject instanceof go.Part;
        var data = isPart ? this.inspectedObject.data : this.inspectedObject;
        if (!data) return; // 没有数据时切勿尝试更新数据!

        diagram.startTransaction('set all properties');
        for (var name in inspectedProps) {
          var input = inspectedProps[name];
          var value = input.value;

          // 不要更新“ readOnly”数据属性
          var decProp = this.declaredProperties[name];
          if (!this.canEditProperty(name, decProp, this.inspectedObject)) continue;

          // 如果是布尔值,或者以前的值为布尔值,
          // 将值解析为布尔值,然后更新input.value以使其匹配
          var type = '';
          if (decProp !== undefined && decProp.type !== undefined) {
            type = decProp.type;
          }
          if (type === '') {
            var oldval = data[name];
            if (typeof oldval === 'boolean') type = 'boolean'; // infer boolean
            else if (typeof oldval === 'number') type = 'number';
            else if (oldval instanceof go.Point) type = 'point';
            else if (oldval instanceof go.Size) type = 'size';
            else if (oldval instanceof go.Rect) type = 'rect';
            else if (oldval instanceof go.Spot) type = 'spot';
            else if (oldval instanceof go.Margin) type = 'margin';
          }

          // 转换为特定类型(如果需要)
          switch (type) {
            case 'boolean':
              value = !(value === false || value === 'false' || value === '0');
              break;
            case 'number':
              value = parseFloat(value);
              break;
            case 'arrayofnumber':
              value = this.convertToArrayOfNumber(value);
              break;
            case 'point':
              value = go.Point.parse(value);
              break;
            case 'size':
              value = go.Size.parse(value);
              break;
            case 'rect':
              value = go.Rect.parse(value);
              break;
            case 'spot':
              value = go.Spot.parse(value);
              break;
            case 'margin':
              value = go.Margin.parse(value);
              break;
            case 'checkbox':
              value = input.checked;
              break;
            case 'select':
              value = decProp.choicesArray[input.selectedIndex];
              break;
          }

          // 在解析为不同的情况下(例如在布尔值的情况下),
          // 显示的值应与实际值匹配
          input.value = value;

          // 以可撤消的方式修改数据对象
          diagram.model.setDataProperty(data, name, value);

          // notify any listener
          if (this.propertyModified !== null) this.propertyModified(name, value, this);
        }
        diagram.commitTransaction('set all properties');
      } else { // 以可撤消的方式修改数据对象
        diagram.startTransaction('set all properties');
        for (var name in inspectedProps) {
          var input = inspectedProps[name];
          var value = input.value;
          var arr1 = value.split('|');
          var arr2 = [];
          if (this._multipleProperties[name]) {
            // 如果它是联合及其复选框类型,则不要拆分
            if (this.declaredProperties[name] && this.declaredProperties[name].type === 'checkbox' && this
              .showAllProperties) {
              arr2.push(this._multipleProperties[name]);
            } else {
              arr2 = this._multipleProperties[name].toString().split('|');
            }
          }
          var it = diagram.selection.iterator;
          var change = false;
          if (this.declaredProperties[name] && this.declaredProperties[name].type === 'checkbox') change =
            true; // always change checkbox
          if (arr1.length < arr2.length // 即Alpha | Beta-> Alpha进行更改
            &&
            (!this.declaredProperties[name] // from and to links
              ||
              !(this.declaredProperties[name] // 不要更改颜色复选框和选项,因为它们总是少
                &&
                (this.declaredProperties[name].type === 'color' || this.declaredProperties[name].type ===
                  'checkbox' || this.declaredProperties[name].type === 'choices')))) {
            change = true;
          } else { // 特性变化的标准检测
            for (var j = 0; j < arr1.length && j < arr2.length; j++) {
              if (!(arr1[j] === arr2[j]) &&
                !(this.declaredProperties[name] && this.declaredProperties[name].type === 'color' && arr1[j]
                  .toLowerCase() === arr2[j].toLowerCase())) {
                change = true;
              }
            }
          }
          if (change) { // 只更改需要更改的属性,而不是全部更改
            for (var i = 0; i < diagram.selection.count; i++) {
              it.next();
              var isPart = it.value instanceof go.Part;
              var data = isPart ? it.value.data : it.value;

              if (data) { // 如果没有数据,则忽略所选节点
                if (i < arr1.length) value = arr1[i];
                else value = arr1[0];

                // 不要更新“ readOnly”数据属性
                var decProp = this.declaredProperties[name];
                if (!this.canEditProperty(name, decProp, it.value)) continue;

                // 如果是布尔值,或者以前的值为布尔值,
                // 将值解析为布尔值,然后更新input.value以使其匹配
                var type = '';
                if (decProp !== undefined && decProp.type !== undefined) {
                  type = decProp.type;
                }
                if (type === '') {
                  var oldval = data[name];
                  if (typeof oldval === 'boolean') type = 'boolean'; // infer boolean
                  else if (typeof oldval === 'number') type = 'number';
                  else if (oldval instanceof go.Point) type = 'point';
                  else if (oldval instanceof go.Size) type = 'size';
                  else if (oldval instanceof go.Rect) type = 'rect';
                  else if (oldval instanceof go.Spot) type = 'spot';
                  else if (oldval instanceof go.Margin) type = 'margin';
                }

                // 转换为特定类型(如果需要)
                switch (type) {
                  case 'boolean':
                    value = !(value === false || value === 'false' || value === '0');
                    break;
                  case 'number':
                    value = parseFloat(value);
                    break;
                  case 'arrayofnumber':
                    value = this.convertToArrayOfNumber(value);
                    break;
                  case 'point':
                    value = go.Point.parse(value);
                    break;
                  case 'size':
                    value = go.Size.parse(value);
                    break;
                  case 'rect':
                    value = go.Rect.parse(value);
                    break;
                  case 'spot':
                    value = go.Spot.parse(value);
                    break;
                  case 'margin':
                    value = go.Margin.parse(value);
                    break;
                  case 'checkbox':
                    value = input.checked;
                    break;
                  case 'select':
                    value = decProp.choicesArray[input.selectedIndex];
                    break;
                }

                // 在解析为不同的情况下(例如在布尔值的情况下),
                // 显示的值应与实际值匹配
                input.value = value;

                // 以可撤消的方式修改数据对象
                diagram.model.setDataProperty(data, name, value);

                // 通知所有
                if (this.propertyModified !== null) this.propertyModified(name, value, this);
              }
            }
          }
        }
        diagram.commitTransaction('set all properties');
      }
    };

    //--------------------------------------------------------------------
  </script>
</head>

<body onload="init()">
  <div id="sample">
    <div id="myDiagramDiv" style="background-color: #34343C; border: solid 1px black; height: 570px;"></div>
    <p><button id="zoomToFit">缩放 <button id="centerRoot">居中</button></p>
    <div>
      <button id="SaveButton" onclick="save()">Save</button>
      <button onclick="load()">Load</button>
    </div>
    <div>
      <div id="myInspector">
      </div>
    </div>
    <p>
      此可编辑的组织结构图示例根据层次结构中的树级别对节点进行颜色编码。 图表模型以JSON格式保存
    </p>
    <p>
      双击节点以添加人或图背景以添加新的老板。 双击该图使用ClickCreatingTool
      使用自定义的ClickCreatingTool.insertPart滚动到新节点并开始编辑TextBlock的名称。
    </p>
    <p>
      将一个节点拖到另一个节点上以更改关系。
      您还可以绘制从节点背景到其他没有“老板”的节点的链接。 链接也可以重新链接以更改关系。
      右键单击或点住节点以显示上下文菜单,该菜单使您可以:
      <ul>
        <li>空缺职位-删除特定于该职位的当前人员的信息</li>
        <li>删除角色-完全删除该角色并为所有子项添加父项</li>
        <li>删除部门-删除角色和整个子树</li>
      </ul>
      删除节点或链接将孤立子节点并生成新树。 自定义的SelectionDeleting DiagramEvent侦听器将清除老板信息
      当父母被删除时。
    </p>
    <p>
      选择一个节点以编辑/更新节点数据值。 该示例使用Data Inspector扩展来显示和修改零件数据。
    </p>
    <div>
    </div>
  </div>
</body>

</html>

  GoJS事件https://blog.csdn.net/pdw2009/article/details/82993971

学习 https://www.jianshu.com/p/f91fbf085574

原文地址:https://www.cnblogs.com/Alwaysbecoding/p/11851592.html