UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

499 lines (449 loc) 19.5 kB
"use strict"; /* * Copyright (C) 1998-2018 by Northwoods Software Corporation. All Rights Reserved. */ /** This class implements an inspector for GoJS model data objects. The constructor takes three arguments: {string} divid a string referencing the HTML ID of the to-be inspector's div. {Diagram} diagram a reference to a GoJS Diagram. {Object} options An optional JS Object describing options for the inspector. Options: inspectSelection {boolean} Default true, whether to automatically show and populate the Inspector with the currently selected Diagram Part. If set to false, the inspector won't show anything until you call Inspector.inspectObject(object) with a Part or JavaScript object as the argument. includesOwnProperties {boolean} Default true, whether to list all properties currently on the inspected data object. properties {Object} An object of string:Object pairs representing propertyName:propertyOptions. Can be used to include or exclude additional properties. propertyModified function(propertyName, newValue) a callback Options for properties: show: {boolean|function} a boolean value to show or hide the property from the inspector, or a predicate function to show conditionally. readOnly: {boolean|function} whether or not the property is read-only type: {string} a string describing the data type. Supported values: "string|number|boolean|color|arrayofnumber|point|rect|size|spot|margin|select" defaultValue: {*} a default value for the property. Defaults to the empty string. choices: {Array|function} when type == "select", the Array of choices to use or a function that returns the Array of choices. Example usage of 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"] } } }); This is the basic HTML Structure that the Inspector creates within the given DIV element: <div id="divid" class="inspector"> <tr> <td>propertyName</td> <td><input value=propertyValue /></td> </tr> ... </div> */ /* THIS VERSION OF DATAINSPECTOR HAS BEEN MODIFIED FOR USE WITH GOTIMELINE Notable changes: - Only declared properties are displayed in the inspector - JavaScript Date type inputs are supported - If nothing is being inspected, no table is shown */ function Inspector(divid, diagram, options) { var mainDiv = document.getElementById(divid); mainDiv.className = "inspector"; mainDiv.innerHTML = ""; this._div = mainDiv; this._diagram = diagram; this._inspectedProperties = {}; // 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; 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"]; } 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 = object; if (inspectedObject === undefined) { if (this.inspectsSelection) inspectedObject = this._diagram.selection.first(); else inspectedObject = this.inspectedObject; } if (inspectedObject === null || this.inspectedObject === inspectedObject) { this.inspectedObject = inspectedObject; this.updateAllHTML(); return; } this.inspectedObject = inspectedObject; if (inspectedObject === 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 k in declaredProperties) { var val = declaredProperties[k]; if (!this.canShowProperty(k, val, inspectedObject)) continue; var defaultValue = ""; if (val.defaultValue !== undefined) defaultValue = val.defaultValue; if (data[k] !== undefined) defaultValue = data[k]; var row = this.buildPropertyRow(k, defaultValue || ""); if (row) { tbody.appendChild(row); } } // 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; var row = this.buildPropertyRow(k, data[k]); if (row) { tbody.appendChild(row); } } } table.appendChild(tbody); mainDiv.appendChild(table); }; /** * @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 * 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 pNameMap = new go.Map(); pNameMap.add("isPositionedBelow", "Positioned Below Timeline?"); pNameMap.add("event", "Event"); pNameMap.add("date", "Date"); pNameMap.add("description", "Description"); pNameMap.add("start", "Start Date"); pNameMap.add("end", "End Date"); pNameMap.add("color", "Color"); var td1 = document.createElement("td"); var pName = pNameMap.get(propertyName) !== null ? pNameMap.get(propertyName) : propertyName; td1.textContent = pName; tr.appendChild(td1); var td2 = document.createElement("td"); var decProp = this.declaredProperties[propertyName]; if (decProp == undefined) return null; 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 = (decProp.type == "textarea") ? document.createElement("textarea") : document.createElement("input"); // Date values must be handled with care if (decProp.type === "date") { input.value = propertyValue.toISOString().substring(0,10); } else { 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 let 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 = ""; } if (this._div.children.length !== 0) { this._div.removeChild(this._div.children[0]); } this._div.innerText = "Select an object to edit its properties"; } } 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 if (input.type === "date") { input.value = propertyValue.toISOString().substring(0,10); } 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; 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 var dateValue = null; 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; case "date": { dateValue = makeLocalDate(value); // this function is defined in timeline-data-edit.js } } // 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 if (dateValue) { diagram.model.setDataProperty(data, name, dateValue); } else { diagram.model.setDataProperty(data, name, value); } // if the date value provided is outside the boundaries of the timeline, update the boundaries of the timeline var timeline = diagram.findNodeForKey("timeline"); if (dateValue && dateValue < timeline.data.start) { diagram.model.set(timeline.data, "start", dateValue); } if (dateValue && dateValue > timeline.data.end) { diagram.model.set(timeline.data, "end", dateValue); } // notify any listener if (this.propertyModified !== null) this.propertyModified(name, value, this); } diagram.commitTransaction("set all properties"); };