UNPKG

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