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 | */
|
11 | import * 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 | */
|
64 | export 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 | }
|