GoJS组织结构图2
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>组织结构图</title> <meta name="description" content="An organization chart editor -- edit details and change relationships." /> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Copyright 1998-2019 by Northwoods Software Corporation. --> <style> .inspector { display: inline-block; font: bold 14px helvetica, sans-serif; background-color: #212121; /* Grey 900 */ color: #F5F5F5; /* Grey 100 */ cursor: default; } .inspector table { border-collapse: separate; border-spacing: 2px; } .inspector td, th { padding: 2px; } .inspector input { background-color: #424242; /* Grey 800 */ color: #F5F5F5; /* Grey 100 */ font: bold 12px helvetica, sans-serif; border: 0px; padding: 2px; } .inspector input:disabled { background-color: #BDBDBD; /* Grey 400 */ color: #616161; /* Grey 700 */ } .inspector select { background-color: #424242; } </style> <script src="https://cdnjs.cloudflare.com/ajax/libs/gojs/1.8.13/go-debug.js"></script> <!-- this is only for the GoJS Samples framework --> </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">Zoom to Fit</button> <button id="centerRoot">Center on root</button> </p> <div> <div id="myInspector"> </div> </div> <p> This editable organizational chart sample color-codes the Nodes according to the tree level in the hierarchy. </p> <p> Double click on a node in order to add a person or the diagram background to add a new boss. Double clicking the diagram uses the <a>ClickCreatingTool</a> with a custom <a>ClickCreatingTool.insertPart</a> to scroll to the new node and start editing the <a>TextBlock</a> for its name . </p> <p> Drag a node onto another in order to change relationships. You can also draw a link from a node's background to other nodes that have no "boss". Links can also be relinked to change relationships. Right-click or tap-hold a Node to bring up a context menu which allows you to: <ul> <li>Vacate Position - remove the information specfic to the current person in that role</li> <li>Remove Role - removes the role entirely and reparents any children</li> <li>Remove Department - removes the role and the whole subtree</li> </ul> Deleting a Node or Link will orphan the child Nodes and generate a new tree. A custom SelectionDeleting <a>DiagramEvent</a> listener will clear out the boss info when the parent is removed. </p> <p> Select a node to edit/update node data values. This sample uses the <a href="../extensions/dataInspector.html">Data Inspector</a> extension to display and modify Part data. </p> <p> To learn how to build an org chart from scratch with GoJS, see the <a href="../learn/index.html">Getting Started tutorial</a>. </p> <p> If you want to have some "assistant" nodes on the side, above the regular reports, see the <a href="orgChartAssistants.html">Org Chart Assistants</a> sample, which is a copy of this sample that uses a custom <a>TreeLayout</a> to position "assistants" that way. </p> <div> <div> <button id="SaveButton" onclick="save()">Save</button> <button onclick="load()">Load</button> Diagram Model saved in JSON format: </div> <textarea id="mySavedModel" style="width:100%; height:270px;"> { "class": "go.TreeModel", "nodeDataArray": [ {"key":1, "name":"中心主题"} ] } </textarea> </div> </div> </body> <script id="code"> function init() { if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this var $ = go.GraphObject.make; //GraphObject是所有图形基类,这里简洁定义模板,$太敏感,为避免与jQuery冲突,此处使用ds //图表由节点、文字、线、箭头组成。 myDiagram = $(go.Diagram, "myDiagramDiv", // must be the ID or reference to div { "initialContentAlignment": go.Spot.Center, // 将图表在画布中居中显示 maxSelectionCount: 1, // 一次允许选择一个部件, //"isReadOnly": true, // 只读 "allowZoom": true, //画布缩放 //"InitialLayoutCompleted":loadDiagramProperties这是函数名, //一个DiagramEvent侦听器 //鼠标滚轮事件放大和缩小,而不是向上和向下滚动 "toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom, "undoManager.isEnabled": true, // 操作支持Ctrl-Z撤销、Ctrl-Y重做 validCycle: go.Diagram.CycleDestinationTree, // make sure users can only create trees //"clickCreatingTool.archetypeNodeData": { // 默认单根节点,这段代码允许多根节点,双击背景创建新的根节点 // name: "(自由节点)" //}, "clickCreatingTool.insertPart": function (loc) { // scroll to the new node 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, // 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 }) }); // 当文档被修改时,在标题中添加“*”并启用“Save”按钮 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); } }); // manage boss info manually when a node or link is deleted from the diagram myDiagram.addDiagramListener("SelectionDeleting", function (e) { var part = e.subject.first(); // e.subject is the myDiagram.selection collection, // so we'll get the first since we know we only have one selection myDiagram.startTransaction("clear boss"); if (part instanceof go.Node) { var it = part.findTreeChildrenNodes(); // find all child nodes while (it.next()) { // now iterate through them and clear out the boss information var child = it.value; var bossText = child.findObject("boss"); // since the boss TextBlock is named, we can access it by name if (bossText === null) return; bossText.text = ""; } } else if (part instanceof go.Link) { var child = part.toNode; var bossText = child.findObject("boss"); // since the boss TextBlock is named, we can access it by name if (bossText === null) return; bossText.text = ""; } myDiagram.commitTransaction("clear boss"); }); var levelColors = ["#AC193D", "#2672EC", "#8C0095", "#5133AB", "#008299", "#D24726", "#008A00", "#094AB2"]; // override TreeLayout.commitNodes to also modify the background brush based on the tree depth level myDiagram.layout.commitNodes = function () { go.TreeLayout.prototype.commitNodes.call(myDiagram.layout); // do the standard behavior // then go through all of the vertexes and set their corresponding node's Shape.fill // to a brush dependent on the TreeVertex.level value 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 }); } }); }; // when a node is double-clicked, add a child to it function createNode(e, obj) { //拿到节点的对象,后面要拿什么值就直接拿 var clicked = obj.part; if (clicked !== null) { var thisemp = clicked.data; myDiagram.startTransaction("createNode"); var newemp = { name: "(新节点)", parent: thisemp.key }; //console.log(thisemp.key); console.log(myDiagram.model.toJson()); myDiagram.model.addNodeData(newemp); myDiagram.commitTransaction("createNode"); } } // this is used to determine feedback during drags function mayWorkFor(node1, node2) { if (!(node1 instanceof go.Node)) return false; // must be a Node if (node1 === node2) return false; // cannot work for yourself if (node2.isInTreeOf(node1)) return false; // cannot work for someone who works for you return true; } // 为大多数文本块提供通用样式 // Some of these values may be overridden in a particular TextBlock.某些值可能在特定的文本块中被覆盖 function textStyle() { //strokez颜色,添加textAlign: "center"好像效果可能是被覆盖了 return { font: "9pt Segoe UI,sans-serif", stroke: "white" }; } // This converter is used by the Picture. function findHeadShot(key) { if (key < 0 || key > 16) return "images/HSnopic.jpg"; // There are only 16 images on the server return "images/HS" + key + ".jpg" } // define the Node template 定义节点模板 描述如何构建每个节点 myDiagram.nodeTemplate = $(go.Node, "Auto",//形状自动填充适合 与css设置width:auto同样效果 //{ doubleClick: createNode }, { // handle dragging a Node onto a Node to (maybe) change the reporting relationship 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; // remember the original brush shape.fill = "darkred"; } }, mouseDragLeave: function (e, node, next) { var shape = node.findObject("SHAPE"); if (shape && shape._prevFill) { shape.fill = shape._prevFill; // restore the original brush } }, mouseDrop: function (e, node) { var diagram = node.diagram; var selnode = diagram.selection.first(); // assume just one Node in selection if (mayWorkFor(selnode, node)) { // find any existing link into the selected node var link = selnode.findTreeParentLink(); if (link !== null) { // 重新连接所有已有连接 link.fromNode = node; } else { // 新建连接 diagram.toolManager.linkingTool.insertLink(node, node.port, selnode, selnode.port); } } } }, // for sorting, have the Node.text be the data.name new go.Binding("text", "name"), // bind the Part.layerName to control the Node's layer depending on whether it isSelected new go.Binding("layerName", "isSelected", function (sel) { return sel ? "Foreground" : ""; }).ofObject(), //设置节点形状:长方形 $(go.Shape, "Rectangle", { name: "SHAPE", fill: "#333333", stroke: 'white', strokeWidth: 3.5, // set the port properties: 是否可连接fromLinkable、toLinkable portId: "", fromLinkable: false, toLinkable: false, cursor: "pointer" }), // Panel 有不同的类型,每个类型表示一种布局,通过不同的坐标系统排列 $(go.Panel, "Horizontal", // 定义文本显示框 $(go.Panel, "Table", { minSize: new go.Size(130, NaN), maxSize: new go.Size(150, NaN), margin: new go.Margin(30),//设置文本和边框距离 defaultAlignment: go.Spot.Center }, $(go.RowColumnDefinition, { column: 2, width: 4 }), // 设置文本节点 $(go.TextBlock, textStyle(), // the name { row: 0, column: 0, columnSpan: 5, font: "12pt Segoe UI,sans-serif", editable: true, isMultiline: false,// editable文本是否可编辑 minSize: new go.Size(10, 16) }, //将节点数据nodeDataArray.name与text建立联系 new go.Binding("text", "name").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; //console.log(node.data.key); if (node !== null) { myDiagram.startTransaction("remove dept"); // 删除单个节点myDiagram.model.removeNodeData(node.data)只要能拿到node.data对象就能删除了; //删除整个子树,包括节点本身 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: createNode }, $(go.TextBlock, "+", // 按钮的内容 { font: "bold 8pt sans-serif" }) ) ); //监听键盘事件 myDiagram.commandHandler.doKeyDown = function () { var e = myDiagram.lastInput; var control = e.control || e.meta; var key = e.key; console.log('key' + key);//Tab和Enter键好像这里取到的名字是空 if (control && (key === 'Z' || key === 'Y')) { console.log('Ctrl+Z/Y'); }; // 取消Del/Backspace删除键的命令关联: //if (key === 'Del' || key === 'Backspace') return; go.CommandHandler.prototype.doKeyDown.call(this); }; // 设置线条和箭头,是否允许拖动连接relinkableFrom,relinkableTo myDiagram.linkTemplate = $(go.Link, go.Link.Orthogonal, { corner: 5, relinkableFrom: false, relinkableTo: false }, $(go.Shape, { strokeWidth: 1.5, stroke: "#F5F5F5" })); // the link shape // 拖拽框选功能 myDiagram.toolManager.dragSelectingTool.box = $(go.Part, { layerName: "Tool", selectable: true }, $(go.Shape, { name: "SHAPE", fill: null, stroke: "chartreuse", strokeWidth: 3 })); // read in the JSON-format data from the "mySavedModel" element load(); // support editing the properties of the selected person in HTML if (window.Inspector) myInspector = new Inspector("myInspector", myDiagram, { properties: { "key": { readOnly: true }, "comments": {} } }); // Setup zoom to fit button document.getElementById('zoomToFit').addEventListener('click', function () { myDiagram.commandHandler.zoomToFit(); }); document.getElementById('centerRoot').addEventListener('click', function () { myDiagram.scale = 1; myDiagram.commandHandler.scrollToPart(myDiagram.findNodeForKey(1)); }); } // end init // Show the diagram's model in JSON format function save() { document.getElementById("mySavedModel").value = myDiagram.model.toJson(); myDiagram.isModified = false; } function load() { // model中的数据每一个js对象都代表着一个相应的模型图中的元素 myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value); // make sure new data keys are unique positive integers 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; }; } "use strict";//严格模式,在开发中使用严格模式能帮助我们早发现错误 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 = {}; // Either a GoJS Part or a simple data object, such as Model.modelData this.inspectedObject = null; // Inspector options defaults: 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(); }); } } // Some static predicates to use with the "show" property. 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 }; // Only show the property if its present. Useful for "key" which will be shown on Nodes and Groups, but normally not on Links Inspector.showIfPresent = function (data, propname) { if (data instanceof go.Part) data = data.data; return typeof data === "object" && data[propname] !== undefined; }; /** * Update the HTML state of this Inspector given the properties of the {@link #inspectedObject}. * @param {Object} object is an optional argument, used when {@link #inspectSelection} is false to * set {@link #inspectedObject} and show and edit that object's properties. */ Inspector.prototype.inspectObject = function (object) { var inspectedObject = null; var inspectedObjects = null; if (object === null) return; if (object === undefined) { if (this.inspectsSelection) { if (this.multipleSelection) { // gets the selection if multiple selection is true inspectedObjects = this._diagram.selection; } else { // otherwise grab the first object inspectedObject = this._diagram.selection.first(); } } else { // if there is a single inspected object inspectedObject = this.inspectedObject; } } else { // if object was passed in as a parameter inspectedObject = object; } if (inspectedObjects && inspectedObjects.count === 1) { inspectedObject = inspectedObjects.first(); } if (inspectedObjects && inspectedObjects.count <= 1) { inspectedObjects = null; } // single object or no objects 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 = ''; // use either the Part.data or the object itself (for model.modelData) var data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject; if (!data) return; // Build table: var table = document.createElement('table'); var tbody = document.createElement('tbody'); this._inspectedProperties = {}; this.tabIndex = 0; var declaredProperties = this.declaredProperties; // Go through all the properties passed in to the inspector and show them, if appropriate: 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)); } // Go through all the properties on the model data and show them, if appropriate: if (this.includesOwnProperties) { for (var k in data) { if (k === '__gohashid') continue; // skip internal GoJS hash property if (this._inspectedProperties[k]) continue; // already exists if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject)) continue; tbody.appendChild(this.buildPropertyRow(k, data[k])); } } table.appendChild(tbody); mainDiv.appendChild(table); } else { // multiple objects selected var mainDiv = this._div; mainDiv.innerHTML = ''; var shared = new go.Map(); // for properties that the nodes have in common var properties = new go.Map(); // for adding properties var all = new go.Map(); // used later to prevent changing properties when unneeded var it = inspectedObjects.iterator; // Build table: 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) { // initial pass to set shared and all // Go through all the properties passed in to the inspector and add them to the map, if appropriate: 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); } } // Go through all the properties on the model data and add them to the map, if appropriate: if (this.includesOwnProperties) { for (var k in data) { if (k === '__gohashid') continue; // skip internal GoJS hash property if (this._inspectedProperties[k]) continue; // already exists 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) { // use either the Part.data or the object itself (for model.modelData) data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject; if (data) { // Go through all the properties passed in to the inspector and add them to properties to add, if appropriate: 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); } } // Go through all the properties on the model data and add them to properties to add, if appropriate: if (this.includesOwnProperties) { for (var k in data) { if (k === '__gohashid') continue; // skip internal GoJS hash property if (this._inspectedProperties[k]) continue; // already exists if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject)) continue; properties.add(k, data[k]); } } } } if (!this.showAllProperties) { // Cleans up shared map with properties that aren't shared between the selected objects // Also adds properties to the add and shared maps if applicable 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]) { // for non-string properties i.e color newVal = shared.get(addIt.key) + '|' + properties.get(addIt.key); shared.set(addIt.key, newVal); } } else { // toRemove array since addIt is still iterating toRemove.push(addIt.key); } } for (var i = 0; i < toRemove.length; i++) { // removes anything that doesn't showAllPropertiess shared.remove(toRemove[i]); all.remove(toRemove[i]); } } else { // Adds missing properties to all with the correct amount of seperators 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]) { // for non-string properties i.e color 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); } } // Adds bars in case properties is not in all 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]) { // for non-string properties i.e color var newVal = all.get(addIt.key) + '|'; all.set(addIt.key, newVal); } } } } nodecount++; } // builds the table property rows and sets multipleProperties to help with updateall var mapIt; if (!this.showAllProperties) mapIt = shared.iterator; else mapIt = all.iterator; while (mapIt.next()) { tbody.appendChild(this.buildPropertyRow(mapIt.key, mapIt.value)); // shows the properties that are allowed } table.appendChild(tbody); mainDiv.appendChild(table); var allIt = all.iterator; while (allIt.next()) { this._multipleProperties[allIt.key] = allIt.value; // used for updateall to know which properties to change } } }; /** * @ignore * This predicate should be false if the given property should not be shown. * Normally it only checks the value of "show" on the property descriptor. * The default value is true. * @param {string} propertyName the property name * @param {Object} propertyDesc the property descriptor * @param {Object} inspectedObject the data object * @return {boolean} whether a particular property should be shown in this Inspector */ 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 * This predicate should be false if the given property should not be editable by the user. * Normally it only checks the value of "readOnly" on the property descriptor. * The default value is true. * @param {string} propertyName the property name * @param {Object} propertyDesc the property descriptor * @param {Object} inspectedObject the data object * @return {boolean} whether a particular property should be shown in this Inspector */ 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} propDesc * @param {any} data * @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 sets this._inspectedProperties[propertyName] and creates the HTML table row: * <tr> * <td>propertyName</td> * <td><input value=propertyValue /></td> * </tr> * @param {string} propertyName the property name * @param {*} propertyValue the property value * @return the table row */ 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 color input will only take hex, * so var HTML5 canvas convert the color into hex format. * This converts "rgb(255, 0, 0)" into "#FF0000", etc. * @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 * Update all of the HTML in this Inspector. */ 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 * Update an HTMLSelectElement with an appropriate list of choices, given the propertyName */ Inspector.prototype.updateSelect = function (decProp, select, propertyName, propertyValue) { select.innerHTML = ""; // clear out anything that was there var choices = decProp.choices; if (typeof choices === "function") choices = choices(this.inspectedObject, propertyName); if (!Array.isArray(choices)) choices = []; decProp.choicesArray = choices; // remember list of actual choice values (not strings) 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 * Update all of the data properties of {@link #inspectedObject} according to the * current values held in the HTML input elements. */ Inspector.prototype.updateAllProperties = function () { var inspectedProps = this._inspectedProperties; var diagram = this._diagram; if (diagram.selection.count === 1 || !this.multipleSelection) { // single object update var isPart = this.inspectedObject instanceof go.Part; var data = isPart ? this.inspectedObject.data : this.inspectedObject; if (!data) return; // must not try to update data when there's no data! diagram.startTransaction('set all properties'); for (var name in inspectedProps) { var input = inspectedProps[name]; var value = input.value; // don't update "readOnly" data properties var decProp = this.declaredProperties[name]; if (!this.canEditProperty(name, decProp, this.inspectedObject)) continue; // If it's a boolean, or if its previous value was boolean, // parse the value to be a boolean and then update the input.value to match 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'; } // convert to specific type, if needed 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; } // in case parsed to be different, such as in the case of boolean values, // the value shown should match the actual value input.value = value; // modify the data object in an undo-able fashion diagram.model.setDataProperty(data, name, value); // notify any listener if (this.propertyModified !== null) this.propertyModified(name, value, this); } diagram.commitTransaction('set all properties'); } else { // selection object update 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]) { // don't split if it is union and its checkbox type 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 // i.e Alpha|Beta -> Alpha procs the change && (!this.declaredProperties[name] // from and to links || !(this.declaredProperties[name] // do not change color checkbox and choices due to them always having less && (this.declaredProperties[name].type === 'color' || this.declaredProperties[name].type === 'checkbox' || this.declaredProperties[name].type === 'choices')))) { change = true; } else { // standard detection in change in properties 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) { // only change properties it needs to change instead all of them 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) { // ignores the selected node if there is no data if (i < arr1.length) value = arr1[i]; else value = arr1[0]; // don't update "readOnly" data properties var decProp = this.declaredProperties[name]; if (!this.canEditProperty(name, decProp, it.value)) continue; // If it's a boolean, or if its previous value was boolean, // parse the value to be a boolean and then update the input.value to match 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'; } // convert to specific type, if needed 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; } // in case parsed to be different, such as in the case of boolean values, // the value shown should match the actual value input.value = value; // modify the data object in an undo-able fashion diagram.model.setDataProperty(data, name, value); // notify any listener if (this.propertyModified !== null) this.propertyModified(name, value, this); } } } } diagram.commitTransaction('set all properties'); } }; </script> </html>