UNPKG

41.9 kBJavaScriptView Raw
1/*
2* Copyright (C) 1998-2021 by Northwoods Software Corporation. All Rights Reserved.
3*/
4/*
5* This is an extension and not part of the main GoJS library.
6* Note that the API for this class may change with any version, even point releases.
7* If you intend to use an extension in production, you should copy the code to your own source directory.
8* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
9* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
10*/
11import * as go from '../release/go-module.js';
12/**
13 * This class implements an inspector for GoJS model data objects.
14 * The constructor takes three arguments:
15 * - `divid` ***string*** a string referencing the HTML ID of the to-be inspector's div
16 * - `diagram` ***Diagram*** a reference to a GoJS Diagram
17 * - `options` ***Object*** an optional JS Object describing options for the inspector
18 *
19 * Options:
20 * - `inspectSelection` ***boolean*** see {@link #inspectSelection}
21 * - `includesOwnProperties` ***boolean*** see {@link #includesOwnProperties}
22 * - `properties` ***Object*** see {@link #properties}
23 * - `propertyModified` ***function(propertyName, newValue, inspector)*** see {@link #propertyModified}
24 * - `multipleSelection` ***boolean*** see {@link #multipleSelection}
25 * - `showUnionProperties` ***boolean*** see {@link #showUnionProperties}
26 * - `showLimit` ***number*** see {@link #showLimit}
27 *
28 * Options for properties:
29 * - `show` ***boolean | function*** a boolean value to show or hide the property from the inspector, or a predicate function to show conditionally.
30 * - `readOnly` ***boolean | function*** whether or not the property is read-only
31 * - `type` ***string*** a string describing the data type. Supported values: "string|number|boolean|color|arrayofnumber|point|rect|size|spot|margin|select"
32 * - `defaultValue` ***any*** a default value for the property. Defaults to the empty string.
33 * - `choices` ***Array | function*** when type === "select", the Array of choices to use or a function that returns the Array of choices.
34 *
35 * Example usage of Inspector:
36 * ```js
37 * var inspector = new Inspector("myInspector", myDiagram,
38 * {
39 * includesOwnProperties: false,
40 * properties: {
41 * "key": { show: Inspector.showIfPresent, readOnly: true },
42 * "comments": { show: Inspector.showIfNode },
43 * "LinkComments": { show: Inspector.showIfLink },
44 * "chosen": { show: Inspector.showIfNode, type: "checkbox" },
45 * "state": { show: Inspector.showIfNode, type: "select", choices: ["Stopped", "Parked", "Moving"] }
46 * }
47 * });
48 * ```
49 *
50 * This is the basic HTML Structure that the Inspector creates within the given DIV element:
51 * ```html
52 * <div id="divid" class="inspector">
53 * <tr>
54 * <td>propertyName</td>
55 * <td><input value=propertyValue /></td>
56 * </tr>
57 * ...
58 * </div>
59 * ```
60 *
61 * If you want to experiment with this extension, try the <a href="../../extensionsJSM/DataInspector.html">Data Inspector</a> sample.
62 * @category Extension
63 */
64export class Inspector {
65 /**
66 * Constructs an Inspector and sets up properties based on the options provided.
67 * Also sets up change listeners on the Diagram so the Inspector stays up-to-date.
68 * @param {string} divid a string referencing the HTML ID of the to-be Inspector's div
69 * @param {Diagram} diagram a reference to a GoJS Diagram
70 * @param {Object=} options an optional JS Object describing options for the inspector
71 */
72 constructor(divid, diagram, options) {
73 this._inspectedObject = null;
74 // Inspector options defaults:
75 this._inspectSelection = true;
76 this._includesOwnProperties = true;
77 this._properties = {};
78 this._propertyModified = null;
79 this._multipleSelection = false;
80 this._showUnionProperties = false;
81 this._showLimit = 0;
82 // Private variables used to keep track of internal state
83 this.inspectedProperties = {};
84 this.multipleProperties = {};
85 const mainDiv = document.getElementById(divid);
86 mainDiv.className = 'inspector';
87 mainDiv.innerHTML = '';
88 this._div = mainDiv;
89 this._diagram = diagram;
90 this.tabIndex = 0;
91 // Set properties based on options
92 if (options !== undefined) {
93 if (options.inspectSelection !== undefined)
94 this._inspectSelection = options.inspectSelection;
95 if (options.includesOwnProperties !== undefined)
96 this._includesOwnProperties = options.includesOwnProperties;
97 if (options.properties !== undefined)
98 this._properties = options.properties;
99 if (options.propertyModified !== undefined)
100 this._propertyModified = options.propertyModified;
101 if (options.multipleSelection !== undefined)
102 this._multipleSelection = options.multipleSelection;
103 if (options.showUnionProperties !== undefined)
104 this._showUnionProperties = options.showUnionProperties;
105 if (options.showLimit !== undefined)
106 this._showLimit = options.showLimit;
107 }
108 // Prepare change listeners
109 const self = this;
110 this.inspectOnModelChanged = (e) => {
111 if (e.isTransactionFinished)
112 self.inspectObject();
113 };
114 this.inspectOnSelectionChanged = (e) => { self.inspectObject(); };
115 this._diagram.addModelChangedListener(this.inspectOnModelChanged);
116 if (this._inspectSelection) {
117 this._diagram.addDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
118 }
119 }
120 /**
121 * This read-only property returns the HTMLElement containing the Inspector.
122 */
123 get div() { return this._div; }
124 /**
125 * Gets or sets the {@link Diagram} associated with this Inspector.
126 */
127 get diagram() { return this._diagram; }
128 set diagram(val) {
129 if (val !== this._diagram) {
130 // First, unassociate change listeners with current inspected diagram
131 this._diagram.removeModelChangedListener(this.inspectOnModelChanged);
132 this._diagram.removeDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
133 // Now set the diagram and add the necessary change listeners
134 this._diagram = val;
135 this._diagram.addModelChangedListener(this.inspectOnModelChanged);
136 if (this._inspectSelection) {
137 this._diagram.addDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
138 this.inspectObject();
139 }
140 else {
141 this.inspectObject(null);
142 }
143 }
144 }
145 /**
146 * This read-only property returns the object currently being inspected.
147 *
148 * To set the inspected object, call {@link #inspectObject}.
149 */
150 get inspectedObject() { return this._inspectedObject; }
151 /**
152 * Gets or sets whether the Inspector automatically inspects the associated Diagram's selection.
153 * When set to false, the Inspector won't show anything until {@link #inspectObject} is called.
154 *
155 * The default value is true.
156 */
157 get inspectSelection() { return this._inspectSelection; }
158 set inspectSelection(val) {
159 if (val !== this._inspectSelection) {
160 this._inspectSelection = val;
161 if (this._inspectSelection) {
162 this._diagram.addDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
163 this.inspectObject();
164 }
165 else {
166 this._diagram.removeDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
167 this.inspectObject(null);
168 }
169 }
170 }
171 /**
172 * Gets or sets whether the Inspector includes all properties currently on the inspected object.
173 *
174 * The default value is true.
175 */
176 get includesOwnProperties() { return this._includesOwnProperties; }
177 set includesOwnProperties(val) {
178 if (val !== this._includesOwnProperties) {
179 this._includesOwnProperties = val;
180 this.inspectObject();
181 }
182 }
183 /**
184 * Gets or sets the properties that the Inspector will inspect, maybe setting options for those properties.
185 * The object should contain string: Object pairs represnting propertyName: propertyOptions.
186 * Can be used to include or exclude additional properties.
187 *
188 * The default value is an empty object.
189 */
190 get properties() { return this._properties; }
191 set properties(val) {
192 if (val !== this._properties) {
193 this._properties = val;
194 this.inspectObject();
195 }
196 }
197 /**
198 * Gets or sets the function to be called when a property is modified by the Inspector.
199 * The first paremeter will be the property name, the second will be the new value, and the third will be a reference to this Inspector.
200 *
201 * The default value is null, meaning nothing will be done.
202 */
203 get propertyModified() { return this._propertyModified; }
204 set propertyModified(val) {
205 if (val !== this._propertyModified) {
206 this._propertyModified = val;
207 }
208 }
209 /**
210 * Gets or sets whether the Inspector displays properties for multiple selected objects or just the first.
211 *
212 * The default value is false, meaning only the first item in the {@link Diagram#selection} is inspected.
213 */
214 get multipleSelection() { return this._multipleSelection; }
215 set multipleSelection(val) {
216 if (val !== this._multipleSelection) {
217 this._multipleSelection = val;
218 this.inspectObject();
219 }
220 }
221 /**
222 * Gets or sets whether the Inspector displays the union or intersection of properties for multiple selected objects.
223 *
224 * The default value is false, meaning the intersection of properties is inspected.
225 */
226 get showUnionProperties() { return this._showUnionProperties; }
227 set showUnionProperties(val) {
228 if (val !== this._showUnionProperties) {
229 this._showUnionProperties = val;
230 this.inspectObject();
231 }
232 }
233 /**
234 * Gets or sets how many objects will be displayed when {@link #multipleSelection} is true.
235 *
236 * The default value is 0, meaning all selected objects will be displayed for a given property.
237 */
238 get showLimit() { return this._showLimit; }
239 set showLimit(val) {
240 if (val !== this._showLimit) {
241 this._showLimit = val;
242 this.inspectObject();
243 }
244 }
245 /**
246 * This predicate function can be used as a value for the `show` option for properties.
247 * When used, the property will only be shown when inspecting a {@link Node}.
248 * @param {Part} part the Part being inspected
249 * @return {boolean}
250 */
251 static showIfNode(part) { return part instanceof go.Node; }
252 /**
253 * This predicate function can be used as a value for the `show` option for properties.
254 * When used, the property will only be shown when inspecting a {@link Link}.
255 * @param {Part} part the Part being inspected
256 * @return {boolean}
257 */
258 static showIfLink(part) { return part instanceof go.Link; }
259 /**
260 * This predicate function can be used as a value for the `show` option for properties.
261 * When used, the property will only be shown when inspecting a {@link Group}.
262 * @param {Part} part the Part being inspected
263 * @return {boolean}
264 */
265 static showIfGroup(part) { return part instanceof go.Group; }
266 /**
267 * This predicate function can be used as a value for the `show` option for properties.
268 * When used, the property will only be shown if present.
269 * Useful for properties such as `key`, which will be shown on Nodes and Groups, but normally not on Links
270 * @param {Part|null} part the Part being inspected
271 * @param {string} propname the property to check presence of
272 * @return {boolean}
273 */
274 static showIfPresent(data, propname) {
275 if (data instanceof go.Part)
276 data = data.data;
277 return typeof data === 'object' && data[propname] !== undefined;
278 }
279 /**
280 * Update the HTML state of this Inspector with the given object.
281 *
282 * If passed an object, the Inspector will inspect that object.
283 * If passed null, this will do nothing.
284 * If no parameter is supplied, the {@link #inspectedObject} will be set based on the value of {@link #inspectSelection}.
285 * @param {Object=} object an optional argument, used when {@link #inspectSelection} is false to
286 * set {@link #inspectedObject} and show and edit that object's properties.
287 */
288 inspectObject(object) {
289 let inspectedObject = null;
290 let inspectedObjects = null;
291 if (object === null)
292 return;
293 if (object === undefined) {
294 if (this._inspectSelection) {
295 if (this._multipleSelection) { // gets the selection if multiple selection is true
296 inspectedObjects = this._diagram.selection;
297 }
298 else { // otherwise grab the first object
299 inspectedObject = this._diagram.selection.first();
300 }
301 }
302 else { // if there is a single inspected object
303 inspectedObject = this._inspectedObject;
304 }
305 }
306 else { // if object was passed in as a parameter
307 inspectedObject = object;
308 }
309 if (!inspectedObjects && inspectedObject) {
310 inspectedObjects = new go.Set();
311 inspectedObjects.add(inspectedObject);
312 }
313 if (!inspectedObjects || inspectedObjects.count < 1) { // if nothing is selected
314 this.updateAllHTML();
315 return;
316 }
317 if (inspectedObjects) {
318 const mainDiv = this._div;
319 mainDiv.innerHTML = '';
320 const shared = new go.Map(); // for properties that the nodes have in common
321 const properties = new go.Map(); // for adding properties
322 const all = new go.Map(); // used later to prevent changing properties when unneeded
323 const it = inspectedObjects.iterator;
324 let nodecount = 2;
325 // Build table:
326 const table = document.createElement('table');
327 const tbody = document.createElement('tbody');
328 this.inspectedProperties = {};
329 this.tabIndex = 0;
330 const declaredProperties = this._properties;
331 it.next();
332 inspectedObject = it.value;
333 this._inspectedObject = inspectedObject;
334 let data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
335 if (data) { // initial pass to set shared and all
336 // Go through all the properties passed in to the inspector and add them to the map, if appropriate:
337 for (const name in declaredProperties) {
338 const desc = declaredProperties[name];
339 if (!this.canShowProperty(name, desc, inspectedObject))
340 continue;
341 const val = this.findValue(name, desc, data);
342 if (val === '' && this._properties[name] && this._properties[name].type === 'checkbox') {
343 shared.add(name, false);
344 all.add(name, false);
345 }
346 else {
347 shared.add(name, val);
348 all.add(name, val);
349 }
350 }
351 // Go through all the properties on the model data and add them to the map, if appropriate:
352 if (this._includesOwnProperties) {
353 for (const k in data) {
354 if (k === '__gohashid')
355 continue; // skip internal GoJS hash property
356 if (this.inspectedProperties[k])
357 continue; // already exists
358 if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject))
359 continue;
360 shared.add(k, data[k]);
361 all.add(k, data[k]);
362 }
363 }
364 }
365 while (it.next() && (this._showLimit < 1 || nodecount <= this._showLimit)) { // grabs all the properties from the other selected objects
366 properties.clear();
367 inspectedObject = it.value;
368 if (inspectedObject) {
369 // use either the Part.data or the object itself (for model.modelData)
370 data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
371 if (data) {
372 // Go through all the properties passed in to the inspector and add them to properties to add, if appropriate:
373 for (const name in declaredProperties) {
374 const desc = declaredProperties[name];
375 if (!this.canShowProperty(name, desc, inspectedObject))
376 continue;
377 const val = this.findValue(name, desc, data);
378 if (val === '' && this._properties[name] && this._properties[name].type === 'checkbox') {
379 properties.add(name, false);
380 }
381 else {
382 properties.add(name, val);
383 }
384 }
385 // Go through all the properties on the model data and add them to properties to add, if appropriate:
386 if (this._includesOwnProperties) {
387 for (const k in data) {
388 if (k === '__gohashid')
389 continue; // skip internal GoJS hash property
390 if (this.inspectedProperties[k])
391 continue; // already exists
392 if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject))
393 continue;
394 properties.add(k, data[k]);
395 }
396 }
397 }
398 }
399 if (!this._showUnionProperties) {
400 // Cleans up shared map with properties that aren't shared between the selected objects
401 // Also adds properties to the add and shared maps if applicable
402 const addIt = shared.iterator;
403 const toRemove = [];
404 while (addIt.next()) {
405 if (properties.has(addIt.key)) {
406 let newVal = all.get(addIt.key) + '|' + properties.get(addIt.key);
407 all.set(addIt.key, newVal);
408 if ((declaredProperties[addIt.key] && declaredProperties[addIt.key].type !== 'color'
409 && declaredProperties[addIt.key].type !== 'checkbox' && declaredProperties[addIt.key].type !== 'select')
410 || !declaredProperties[addIt.key]) { // for non-string properties i.e color
411 newVal = shared.get(addIt.key) + '|' + properties.get(addIt.key);
412 shared.set(addIt.key, newVal);
413 }
414 }
415 else { // toRemove array since addIt is still iterating
416 toRemove.push(addIt.key);
417 }
418 }
419 for (let i = 0; i < toRemove.length; i++) { // removes anything that doesn't showUnionProperties
420 shared.remove(toRemove[i]);
421 all.remove(toRemove[i]);
422 }
423 }
424 else {
425 // Adds missing properties to all with the correct amount of seperators
426 let addIt = properties.iterator;
427 while (addIt.next()) {
428 if (all.has(addIt.key)) {
429 if ((declaredProperties[addIt.key] && declaredProperties[addIt.key].type !== 'color'
430 && declaredProperties[addIt.key].type !== 'checkbox' && declaredProperties[addIt.key].type !== 'select')
431 || !declaredProperties[addIt.key]) { // for non-string properties i.e color
432 const newVal = all.get(addIt.key) + '|' + properties.get(addIt.key);
433 all.set(addIt.key, newVal);
434 }
435 }
436 else {
437 let newVal = '';
438 for (let i = 0; i < nodecount - 1; i++)
439 newVal += '|';
440 newVal += properties.get(addIt.key);
441 all.set(addIt.key, newVal);
442 }
443 }
444 // Adds bars in case properties is not in all
445 addIt = all.iterator;
446 while (addIt.next()) {
447 if (!properties.has(addIt.key)) {
448 if ((declaredProperties[addIt.key] && declaredProperties[addIt.key].type !== 'color'
449 && declaredProperties[addIt.key].type !== 'checkbox' && declaredProperties[addIt.key].type !== 'select')
450 || !declaredProperties[addIt.key]) { // for non-string properties i.e color
451 const newVal = all.get(addIt.key) + '|';
452 all.set(addIt.key, newVal);
453 }
454 }
455 }
456 }
457 nodecount++;
458 }
459 // builds the table property rows and sets multipleProperties to help with updateall
460 let mapIt;
461 if (!this._showUnionProperties)
462 mapIt = shared.iterator;
463 else
464 mapIt = all.iterator;
465 while (mapIt.next()) {
466 tbody.appendChild(this.buildPropertyRow(mapIt.key, mapIt.value)); // shows the properties that are allowed
467 }
468 table.appendChild(tbody);
469 mainDiv.appendChild(table);
470 const allIt = all.iterator;
471 while (allIt.next()) {
472 this.multipleProperties[allIt.key] = allIt.value; // used for updateall to know which properties to change
473 }
474 }
475 }
476 /**
477 * This predicate should be false if the given property should not be shown.
478 * Normally it only checks the value of "show" on the property descriptor.
479 *
480 * The default value is true.
481 * @param {string} propertyName the property name
482 * @param {Object} propertyDesc the property descriptor
483 * @param {Object} inspectedObject the data object
484 * @return {boolean} whether a particular property should be shown in this Inspector
485 */
486 canShowProperty(propertyName, propertyDesc, inspectedObject) {
487 const prop = propertyDesc;
488 if (prop.show === false)
489 return false;
490 // if "show" is a predicate, make sure it passes or do not show this property
491 if (typeof prop.show === 'function')
492 return prop.show(inspectedObject, propertyName);
493 return true;
494 }
495 /**
496 * This predicate should be false if the given property should not be editable by the user.
497 * Normally it only checks the value of "readOnly" on the property descriptor.
498 *
499 * The default value is true.
500 * @param {string} propertyName the property name
501 * @param {Object} propertyDesc the property descriptor
502 * @param {Object} inspectedObject the data object
503 * @return {boolean} whether a particular property should be shown in this Inspector
504 */
505 canEditProperty(propertyName, propertyDesc, inspectedObject) {
506 if (this._diagram.isReadOnly || this._diagram.isModelReadOnly)
507 return false;
508 if (inspectedObject === null)
509 return false;
510 // assume property values that are functions of Objects cannot be edited
511 const data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
512 const valtype = typeof data[propertyName];
513 if (valtype === 'function')
514 return false;
515 if (propertyDesc) {
516 const prop = propertyDesc;
517 if (prop.readOnly === true)
518 return false;
519 // if "readOnly" is a predicate, make sure it passes or do not show this property
520 if (typeof prop.readOnly === 'function')
521 return !prop.readOnly(inspectedObject, propertyName);
522 }
523 return true;
524 }
525 /**
526 * @ignore
527 * @param propName
528 * @param propDesc
529 * @param data
530 */
531 findValue(propName, propDesc, data) {
532 let val = '';
533 if (propDesc && propDesc.defaultValue !== undefined)
534 val = propDesc.defaultValue;
535 if (data[propName] !== undefined)
536 val = data[propName];
537 if (val === undefined)
538 return '';
539 return val;
540 }
541 /**
542 * This sets `inspectedProperties[propertyName]` and creates the HTML table row for a given property:
543 * ```html
544 * <tr>
545 * <td>propertyName</td>
546 * <td><input value=propertyValue /></td>
547 * </tr>
548 * ```
549 *
550 * This method can be customized to change how an Inspector row is rendered.
551 * @param {string} propertyName the property name
552 * @param {*} propertyValue the property value
553 * @return {HTMLTableRowElement} the table row
554 */
555 buildPropertyRow(propertyName, propertyValue) {
556 const tr = document.createElement('tr');
557 const td1 = document.createElement('td');
558 let displayName;
559 if (this._properties[propertyName] && this._properties[propertyName].name !== undefined) { // name changes the dispaly name shown on inspector
560 displayName = this._properties[propertyName].name;
561 }
562 else {
563 displayName = propertyName;
564 }
565 td1.textContent = displayName;
566 tr.appendChild(td1);
567 const td2 = document.createElement('td');
568 const decProp = this._properties[propertyName];
569 let input = null;
570 const self = this;
571 function updateall() {
572 if (self._diagram.selection.count === 1 || !self.multipleSelection) {
573 self.updateAllProperties();
574 }
575 else {
576 self.updateAllObjectsProperties();
577 }
578 }
579 if (decProp && decProp.type === 'select') {
580 const inputs = input = document.createElement('select');
581 this.updateSelect(decProp, inputs, propertyName, propertyValue);
582 inputs.addEventListener('change', updateall);
583 }
584 else {
585 const inputi = input = document.createElement('input');
586 if (inputi && inputi.setPointerCapture) {
587 inputi.addEventListener("pointerdown", e => inputi.setPointerCapture(e.pointerId));
588 }
589 inputi.value = this.convertToString(propertyValue);
590 if (decProp) {
591 const t = decProp.type;
592 if (t !== 'string' && t !== 'number' && t !== 'boolean' &&
593 t !== 'arrayofnumber' && t !== 'point' && t !== 'size' &&
594 t !== 'rect' && t !== 'spot' && t !== 'margin') {
595 inputi.setAttribute('type', decProp.type);
596 }
597 if (decProp.type === 'color') {
598 if (inputi.type === 'color') {
599 inputi.value = this.convertToColor(propertyValue);
600 // input.addEventListener('input', updateall); // removed with multi select
601 inputi.addEventListener('change', updateall);
602 }
603 }
604 if (decProp.type === 'checkbox') {
605 inputi.checked = !!propertyValue;
606 inputi.addEventListener('change', updateall);
607 }
608 }
609 if (inputi.type !== 'color')
610 inputi.addEventListener('blur', updateall);
611 }
612 if (input) {
613 input.tabIndex = this.tabIndex++;
614 input.disabled = !this.canEditProperty(propertyName, decProp, this._inspectedObject);
615 td2.appendChild(input);
616 }
617 tr.appendChild(td2);
618 this.inspectedProperties[propertyName] = input;
619 return tr;
620 }
621 /**
622 * @hidden @ignore
623 * HTML5 color input will only take hex,
624 * so let HTML5 canvas convert the color into hex format.
625 * This converts "rgb(255, 0, 0)" into "#FF0000", etc.
626 */
627 convertToColor(propertyValue) {
628 const ctx = document.createElement('canvas').getContext('2d');
629 if (ctx === null)
630 return '#000000';
631 ctx.fillStyle = propertyValue;
632 return ctx.fillStyle;
633 }
634 /**
635 * @hidden @ignore
636 */
637 convertToArrayOfNumber(propertyValue) {
638 if (propertyValue === 'null')
639 return null;
640 const split = propertyValue.split(' ');
641 const arr = [];
642 for (let i = 0; i < split.length; i++) {
643 const str = split[i];
644 if (!str)
645 continue;
646 arr.push(parseFloat(str));
647 }
648 return arr;
649 }
650 /**
651 * @hidden @ignore
652 */
653 convertToString(x) {
654 if (x === undefined)
655 return 'undefined';
656 if (x === null)
657 return 'null';
658 if (x instanceof go.Point)
659 return go.Point.stringify(x);
660 if (x instanceof go.Size)
661 return go.Size.stringify(x);
662 if (x instanceof go.Rect)
663 return go.Rect.stringify(x);
664 if (x instanceof go.Spot)
665 return go.Spot.stringify(x);
666 if (x instanceof go.Margin)
667 return go.Margin.stringify(x);
668 if (x instanceof go.List)
669 return this.convertToString(x.toArray());
670 if (Array.isArray(x)) {
671 let str = '';
672 for (let i = 0; i < x.length; i++) {
673 if (i > 0)
674 str += ' ';
675 const v = x[i];
676 str += this.convertToString(v);
677 }
678 return str;
679 }
680 return x.toString();
681 }
682 /**
683 * @hidden @ignore
684 * Update all of the HTML in this Inspector.
685 */
686 updateAllHTML() {
687 const inspectedProps = this.inspectedProperties;
688 const isPart = this._inspectedObject instanceof go.Part;
689 const data = isPart ? this._inspectedObject.data : this._inspectedObject;
690 if (!data) { // clear out all of the fields
691 for (const name in inspectedProps) {
692 const input = inspectedProps[name];
693 if (input instanceof HTMLSelectElement) {
694 input.innerHTML = '';
695 }
696 else if (input.type === 'color') {
697 input.value = '#000000';
698 }
699 else if (input.type === 'checkbox') {
700 input.checked = false;
701 }
702 else {
703 input.value = '';
704 }
705 }
706 }
707 else {
708 for (const name in inspectedProps) {
709 const input = inspectedProps[name];
710 const propertyValue = data[name];
711 if (input instanceof HTMLSelectElement) {
712 const decProp = this._properties[name];
713 this.updateSelect(decProp, input, name, propertyValue);
714 }
715 else if (input.type === 'color') {
716 input.value = this.convertToColor(propertyValue);
717 }
718 else if (input.type === 'checkbox') {
719 input.checked = !!propertyValue;
720 }
721 else {
722 input.value = this.convertToString(propertyValue);
723 }
724 }
725 }
726 }
727 /**
728 * @hidden @ignore
729 * Update an HTMLSelectElement with an appropriate list of choices, given the propertyName
730 */
731 updateSelect(decProp, select, propertyName, propertyValue) {
732 select.innerHTML = ''; // clear out anything that was there
733 let choices = decProp.choices;
734 if (typeof choices === 'function')
735 choices = choices(this._inspectedObject, propertyName);
736 if (!Array.isArray(choices))
737 choices = [];
738 decProp.choicesArray = choices; // remember list of actual choice values (not strings)
739 for (let i = 0; i < choices.length; i++) {
740 const choice = choices[i];
741 const opt = document.createElement('option');
742 opt.text = this.convertToString(choice);
743 select.add(opt);
744 }
745 select.value = this.convertToString(propertyValue);
746 }
747 parseValue(decProp, value, input, oldval) {
748 // If it's a boolean, or if its previous value was boolean,
749 // parse the value to be a boolean and then update the input.value to match
750 let type = '';
751 if (decProp !== undefined && decProp.type !== undefined) {
752 type = decProp.type;
753 }
754 if (type === '') {
755 if (typeof oldval === 'boolean')
756 type = 'boolean'; // infer boolean
757 else if (typeof oldval === 'number')
758 type = 'number';
759 else if (oldval instanceof go.Point)
760 type = 'point';
761 else if (oldval instanceof go.Size)
762 type = 'size';
763 else if (oldval instanceof go.Rect)
764 type = 'rect';
765 else if (oldval instanceof go.Spot)
766 type = 'spot';
767 else if (oldval instanceof go.Margin)
768 type = 'margin';
769 }
770 // convert to specific type, if needed
771 switch (type) {
772 case 'boolean':
773 value = !(value === false || value === 'false' || value === '0');
774 break;
775 case 'number':
776 value = parseFloat(value);
777 break;
778 case 'arrayofnumber':
779 value = this.convertToArrayOfNumber(value);
780 break;
781 case 'point':
782 value = go.Point.parse(value);
783 break;
784 case 'size':
785 value = go.Size.parse(value);
786 break;
787 case 'rect':
788 value = go.Rect.parse(value);
789 break;
790 case 'spot':
791 value = go.Spot.parse(value);
792 break;
793 case 'margin':
794 value = go.Margin.parse(value);
795 break;
796 case 'checkbox':
797 value = input.checked;
798 break;
799 case 'select':
800 value = decProp.choicesArray[input.selectedIndex];
801 break;
802 }
803 return value;
804 }
805 /**
806 * @hidden @ignore
807 * Update all of the data properties of all the objects in {@link #inspectedObjects} according to the
808 * current values held in the HTML input elements.
809 */
810 updateAllObjectsProperties() {
811 const inspectedProps = this.inspectedProperties;
812 const diagram = this._diagram;
813 diagram.startTransaction('set all properties');
814 for (const name in inspectedProps) {
815 const input = inspectedProps[name];
816 let value = input.value;
817 const arr1 = value.split('|');
818 let arr2 = [];
819 if (this.multipleProperties[name]) {
820 // don't split if it is union and its checkbox type
821 if (this._properties[name] && this._properties[name].type === 'checkbox' && this._showUnionProperties) {
822 arr2.push(this.multipleProperties[name]);
823 }
824 else if (this._properties[name]) {
825 arr2 = this.multipleProperties[name].toString().split('|');
826 }
827 }
828 const it = diagram.selection.iterator;
829 let change = false;
830 if (this._properties[name] && this._properties[name].type === 'checkbox')
831 change = true; // always change checkbox
832 if (arr1.length < arr2.length // i.e Alpha|Beta -> Alpha procs the change
833 && (!this._properties[name] // from and to links
834 || !(this._properties[name] // do not change color checkbox and choices due to them always having less
835 && (this._properties[name].type === 'color' || this._properties[name].type === 'checkbox' || this._properties[name].type === 'choices')))) {
836 change = true;
837 }
838 else { // standard detection in change in properties
839 for (let j = 0; j < arr1.length && j < arr2.length; j++) {
840 if (!(arr1[j] === arr2[j])
841 && !(this._properties[name] && this._properties[name].type === 'color' && arr1[j].toLowerCase() === arr2[j].toLowerCase())) {
842 change = true;
843 }
844 }
845 }
846 if (change) { // only change properties it needs to change instead all of them
847 for (let i = 0; i < diagram.selection.count; i++) {
848 it.next();
849 const isPart = it.value instanceof go.Part;
850 const data = isPart ? it.value.data : it.value;
851 if (data) { // ignores the selected node if there is no data
852 if (i < arr1.length)
853 value = arr1[i];
854 else
855 value = arr1[0];
856 // don't update "readOnly" data properties
857 const decProp = this._properties[name];
858 if (!this.canEditProperty(name, decProp, it.value))
859 continue;
860 const oldval = data[name];
861 value = this.parseValue(decProp, value, input, oldval);
862 // in case parsed to be different, such as in the case of boolean values,
863 // the value shown should match the actual value
864 input.value = value;
865 // modify the data object in an undo-able fashion
866 diagram.model.setDataProperty(data, name, value);
867 // notify any listener
868 if (this.propertyModified !== null)
869 this.propertyModified(name, value, this);
870 }
871 }
872 }
873 }
874 diagram.commitTransaction('set all properties');
875 }
876 /**
877 * @hidden @ignore
878 * Update all of the data properties of {@link #inspectedObject} according to the
879 * current values held in the HTML input elements.
880 */
881 updateAllProperties() {
882 const inspectedProps = this.inspectedProperties;
883 const diagram = this._diagram;
884 const isPart = this.inspectedObject instanceof go.Part;
885 const data = isPart ? this.inspectedObject.data : this.inspectedObject;
886 if (!data)
887 return; // must not try to update data when there's no data!
888 diagram.startTransaction('set all properties');
889 for (const name in inspectedProps) {
890 const input = inspectedProps[name];
891 let value = input.value;
892 // don't update "readOnly" data properties
893 const decProp = this._properties[name];
894 if (!this.canEditProperty(name, decProp, this.inspectedObject))
895 continue;
896 const oldval = data[name];
897 value = this.parseValue(decProp, value, input, oldval);
898 // in case parsed to be different, such as in the case of boolean values,
899 // the value shown should match the actual value
900 input.value = value;
901 // modify the data object in an undo-able fashion
902 diagram.model.setDataProperty(data, name, value);
903 // notify any listener
904 if (this.propertyModified !== null)
905 this.propertyModified(name, value, this);
906 }
907 diagram.commitTransaction('set all properties');
908 }
909}