1
|
|
|
define(['factory', 'class', 'config', 'decimal', 'property_menu_entry', 'mirror', 'label', 'alerts', 'jquery', 'underscore'], |
2
|
|
|
function(Factory, Class, Config, Decimal, PropertyMenuEntry, Mirror, Label, Alerts) { |
3
|
|
|
/** |
4
|
|
|
* Package: Base |
5
|
|
|
*/ |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Function: isNumber |
9
|
|
|
* Small helper function that checks the parameter for being a number and not 'NaN'. |
10
|
|
|
* |
11
|
|
|
* Parameters: |
12
|
|
|
* {Object} number - object to be checked for being a number |
13
|
|
|
* |
14
|
|
|
* Returns: |
15
|
|
|
* A {Boolean} indicating whether the passed parameter is a number. |
16
|
|
|
*/ |
17
|
|
|
var isNumber = function(number) { |
18
|
|
|
return _.isNumber(number) && !_.isNaN(number); |
19
|
|
|
}; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Abstract Class: Property |
23
|
|
|
* Abstract base implementation of a property. A property models a key-value-attribute. It contains e.g. the |
24
|
|
|
* name, cost, probability... of a node, edge or node group. It is only used as a data object and DOES NOT take |
25
|
|
|
* care of its visual representation. |
26
|
|
|
* |
27
|
|
|
* In line with that, properties may have multiple mirrors (<Mirror>) that will reflect the property's |
28
|
|
|
* current value below a mirrorer (<Node>). Labels are special mirrors, that are currently used for edges |
29
|
|
|
* only. Additionally a property has a reference to its <PropertyMenuEntry> which will allow the modification |
30
|
|
|
* of the property value by the user through a visual element (think: text input, checkbox...). |
31
|
|
|
* |
32
|
|
|
* Properties can be declared readonly or hidden, which will accordingly prevent the modification of visual |
33
|
|
|
* display. |
34
|
|
|
* |
35
|
|
|
*/ |
36
|
|
|
var Property = Class.extend({ |
37
|
|
|
owner: undefined, |
38
|
|
|
mirrorers: undefined, |
39
|
|
|
value: undefined, |
40
|
|
|
displayName: '', |
41
|
|
|
mirrors: undefined, |
42
|
|
|
label: undefined, |
43
|
|
|
menuEntry: undefined, |
44
|
|
|
hidden: false, |
45
|
|
|
readonly: false, |
46
|
|
|
partInCompound: undefined, |
47
|
|
|
|
48
|
|
|
init: function(owner, mirrorers, definition) { |
49
|
|
|
jQuery.extend(this, definition); |
50
|
|
|
this.owner = owner; |
51
|
|
|
this.mirrorers = mirrorers; |
52
|
|
|
this.mirrors = []; |
53
|
|
|
this._sanitize() |
54
|
|
|
._setupMirrors() |
55
|
|
|
._setupLabel() |
56
|
|
|
._setupMenuEntry(); |
57
|
|
|
|
58
|
|
|
this._triggerChange(this.value, this); |
59
|
|
|
}, |
60
|
|
|
|
61
|
|
|
menuEntryClass: function() { |
62
|
|
|
throw new SubclassResponsibility(); |
63
|
|
|
}, |
64
|
|
|
|
65
|
|
|
validate: function(value, validationResult) { |
66
|
|
|
throw new SubclassResponsibility(); |
67
|
|
|
}, |
68
|
|
|
|
69
|
|
|
setValue: function(newValue, issuer, propagate) { |
70
|
|
|
// we can't optimize for compound parts because their value does not always reflect the |
71
|
|
|
// value stored in the backend |
72
|
|
|
if ((typeof this.partInCompound === 'undefined' && _.isEqual(this.value, newValue)) || this.readonly) { |
73
|
|
|
return this; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
if (typeof propagate === 'undefined') propagate = true; |
77
|
|
|
|
78
|
|
|
var validationResult = {}; |
79
|
|
|
if (!this.validate(newValue, validationResult)) { |
80
|
|
|
var ErrorClass = validationResult.kind || Error; |
81
|
|
|
throw new ErrorClass(validationResult.message); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
this.value = newValue; |
85
|
|
|
this._triggerChange(newValue, issuer); |
86
|
|
|
|
87
|
|
|
if (propagate) { |
88
|
|
|
//TODO: IS THIS REALLY THE RIGHT WAY TO DO IT? |
89
|
|
|
// (we cannot put the require as dependency of this module, as there is some kind of cyclic dependency |
90
|
|
|
// stopping Node.js to work properly) |
91
|
|
|
var Edge = require('edge'); |
92
|
|
|
var Node = require('node'); |
93
|
|
|
var NodeGroup = require('node_group'); |
94
|
|
|
var properties = {}; |
95
|
|
|
|
96
|
|
|
// compound parts need another format for backend propagation |
97
|
|
|
var value = typeof this.partInCompound === 'undefined' ? newValue : [this.partInCompound, newValue]; |
98
|
|
|
properties[this.name] = value; |
99
|
|
|
|
100
|
|
|
if (this.owner instanceof Edge) { |
101
|
|
|
jQuery(document).trigger(Factory.getModule('Config').Events.EDGE_PROPERTY_CHANGED, [this.owner.id, properties]); |
102
|
|
|
} else if (this.owner instanceof Node) { |
103
|
|
|
jQuery(document).trigger(Factory.getModule('Config').Events.NODE_PROPERTY_CHANGED, [this.owner.id, properties]); |
104
|
|
|
} else if (this.owner instanceof NodeGroup) { |
105
|
|
|
jQuery(document).trigger(Factory.getModule('Config').Events.NODEGROUP_PROPERTY_CHANGED, [this.owner.id, properties]); |
106
|
|
|
} else { |
107
|
|
|
throw new TypeError ('unknown owner class'); |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
return this; |
112
|
|
|
}, |
113
|
|
|
|
114
|
|
|
toDict: function() { |
115
|
|
|
var obj = {}; |
116
|
|
|
obj[this.name] = { 'value': this.value }; |
117
|
|
|
return obj; |
118
|
|
|
}, |
119
|
|
|
|
120
|
|
|
setHidden: function(newHidden) { |
121
|
|
|
this.hidden = newHidden; |
122
|
|
|
jQuery(this).trigger(Factory.getModule('Config').Events.PROPERTY_HIDDEN_CHANGED, [newHidden]); |
123
|
|
|
|
124
|
|
|
return this; |
125
|
|
|
}, |
126
|
|
|
|
127
|
|
|
setReadonly: function(newReadonly) { |
128
|
|
|
this.readonly = newReadonly; |
129
|
|
|
jQuery(this).trigger(Factory.getModule('Config').Events.PROPERTY_READONLY_CHANGED, [newReadonly]); |
130
|
|
|
|
131
|
|
|
return this; |
132
|
|
|
}, |
133
|
|
|
|
134
|
|
|
_sanitize: function() { |
135
|
|
|
var validationResult = {}; |
136
|
|
|
if (!this.validate(this.value, validationResult)) { |
137
|
|
|
var ErrorClass = validationResult.kind || Error; |
138
|
|
|
throw new ErrorClass(validationResult.message); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return this; |
142
|
|
|
}, |
143
|
|
|
|
144
|
|
|
_setupMirrors: function() { |
145
|
|
|
if (typeof this.mirror === 'undefined' || this.mirror === null) return this; |
146
|
|
|
|
147
|
|
|
_.each(this.mirrorers, function(mirrorer) { |
148
|
|
|
this.mirrors.push(Factory.create('Mirror', this, mirrorer.container, this.mirror)); |
149
|
|
|
}.bind(this)); |
150
|
|
|
|
151
|
|
|
return this; |
152
|
|
|
}, |
153
|
|
|
|
154
|
|
|
restoreMirrors: function() { |
155
|
|
|
this._setupMirrors() |
156
|
|
|
._triggerChange(this.value, this); |
157
|
|
|
}, |
158
|
|
|
|
159
|
|
|
removeMirror: function(mirror) { |
160
|
|
|
if (!_.contains(this.mirrors, mirror)) return false; |
161
|
|
|
|
162
|
|
|
mirror.takeDownVisualRepresentation(); |
163
|
|
|
this.mirrors = _.without(this.mirrors, mirror); |
164
|
|
|
return true; |
165
|
|
|
}, |
166
|
|
|
|
167
|
|
|
removeAllMirrors: function() { |
168
|
|
|
_.each(this.mirrors, function(mirror) { |
169
|
|
|
this.removeMirror(mirror); |
170
|
|
|
}.bind(this)); |
171
|
|
|
return true; |
172
|
|
|
}, |
173
|
|
|
|
174
|
|
|
_setupLabel: function() { |
175
|
|
|
if (typeof this.label === 'undefined' || this.label === null) return this; |
176
|
|
|
this.label = Factory.create('Label', this, this.owner.jsPlumbEdge, this.label); |
177
|
|
|
|
178
|
|
|
return this; |
179
|
|
|
}, |
180
|
|
|
|
181
|
|
|
_setupMenuEntry: function() { |
182
|
|
|
this.menuEntry = new (this.menuEntryClass())(this); |
183
|
|
|
|
184
|
|
|
return this; |
185
|
|
|
}, |
186
|
|
|
|
187
|
|
|
_triggerChange: function(value, issuer) { |
188
|
|
|
//TODO: IS THIS REALLY THE RIGHT WAY TO DO IT? |
189
|
|
|
// (we cannot put the following required modules as dependency of this module, as there is some kind of |
190
|
|
|
// cyclic dependency stopping Node.js to work properly |
191
|
|
|
var Edge = require('edge'); |
192
|
|
|
var Node = require('node'); |
193
|
|
|
var NodeGroup = require('node_group'); |
194
|
|
|
|
195
|
|
|
if (this.owner instanceof Node) { |
196
|
|
|
jQuery(this).trigger(Factory.getModule('Config').Events.NODE_PROPERTY_CHANGED, [value, value, issuer]); |
197
|
|
|
} else if (this.owner instanceof Edge) { |
198
|
|
|
jQuery(this).trigger(Factory.getModule('Config').Events.EDGE_PROPERTY_CHANGED, [value, value, issuer]); |
199
|
|
|
} else if (this.owner instanceof NodeGroup) { |
200
|
|
|
jQuery(this).trigger(Factory.getModule('Config').Events.NODEGROUP_PROPERTY_CHANGED, [value, value, issuer]); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
return this; |
204
|
|
|
} |
205
|
|
|
}); |
206
|
|
|
|
207
|
|
|
var Bool = Property.extend({ |
208
|
|
|
menuEntryClass: function() { |
209
|
|
|
return PropertyMenuEntry.BoolEntry; |
210
|
|
|
}, |
211
|
|
|
|
212
|
|
|
validate: function(value, validationResult) { |
213
|
|
|
if (typeof value !== 'boolean') { |
214
|
|
|
validationResult.kind = TypeError; |
215
|
|
|
validationResult.message = 'value must be boolean'; |
216
|
|
|
return false; |
217
|
|
|
} |
218
|
|
|
return true; |
219
|
|
|
}, |
220
|
|
|
|
221
|
|
|
_sanitize: function() { |
222
|
|
|
this.value = typeof this.value === 'undefined' ? this.default : this.value; |
223
|
|
|
return this._super(); |
224
|
|
|
} |
225
|
|
|
}); |
226
|
|
|
|
227
|
|
|
var Choice = Property.extend({ |
228
|
|
|
choices: undefined, |
229
|
|
|
values: undefined, |
230
|
|
|
|
231
|
|
|
menuEntryClass: function(){ |
232
|
|
|
return PropertyMenuEntry.ChoiceEntry; |
233
|
|
|
}, |
234
|
|
|
|
235
|
|
|
init: function(owner, mirrorers, definition) { |
236
|
|
|
definition.values = typeof definition.values === 'undefined' ? definition.choices : definition.values; |
237
|
|
|
this._super(owner, mirrorers, definition); |
238
|
|
|
}, |
239
|
|
|
|
240
|
|
|
validate: function(value, validationResult) { |
241
|
|
|
if (!_.find(this.values, function(val){ return _.isEqual(val, value); }, this)) { |
242
|
|
|
validationResult.kind = ValueError; |
243
|
|
|
validationResult.message = 'no such value ' + value; |
244
|
|
|
return false; |
245
|
|
|
} |
246
|
|
|
return true; |
247
|
|
|
}, |
248
|
|
|
|
249
|
|
|
_sanitize: function() { |
250
|
|
|
this.value = typeof this.value === 'undefined' ? this.default : this.value; |
251
|
|
|
|
252
|
|
|
if (typeof this.choices === 'undefined' || this.choices.length === 0) { |
253
|
|
|
throw new ValueError('there must be at least one choice'); |
254
|
|
|
} else if (this.choices.length != this.values.length) { |
255
|
|
|
throw new ValueError('there must be a value for each choice'); |
256
|
|
|
} else if (!_.find(this.values, function(value){ return _.isEqual(value, this.value); }, this)) { |
257
|
|
|
throw new ValueError('unknown value ' + this.value); |
258
|
|
|
} |
259
|
|
|
return this._super(); |
260
|
|
|
}, |
261
|
|
|
|
262
|
|
|
_triggerChange: function(value, issuer) { |
263
|
|
|
var index = -1; |
264
|
|
|
for (var i = this.values.length - 1; i >=0; --i) { |
265
|
|
|
if (_.isEqual(this.values[i], value)) { |
266
|
|
|
index = i; |
267
|
|
|
break; |
268
|
|
|
} |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
jQuery(this).trigger(Factory.getModule('Config').Events.NODE_PROPERTY_CHANGED, [value, this.choices[i], issuer]); |
272
|
|
|
} |
273
|
|
|
}); |
274
|
|
|
|
275
|
|
|
var Compound = Property.extend({ |
276
|
|
|
parts: undefined, |
277
|
|
|
|
278
|
|
|
menuEntryClass: function(){ |
279
|
|
|
return PropertyMenuEntry.CompoundEntry; |
280
|
|
|
}, |
281
|
|
|
|
282
|
|
|
setHidden: function(newHidden) { |
283
|
|
|
this._super(); |
284
|
|
|
this.parts[this.value].setHidden(newHidden); |
285
|
|
|
|
286
|
|
|
return this; |
287
|
|
|
}, |
288
|
|
|
|
289
|
|
|
setReadonly: function(newReadonly) { |
290
|
|
|
this._super(); |
291
|
|
|
_.invoke(this.parts, 'setReadonly', newReadonly); |
292
|
|
|
|
293
|
|
|
return this; |
294
|
|
|
}, |
295
|
|
|
|
296
|
|
|
setValue: function(newValue, propagate) { |
297
|
|
|
if (typeof propagate === 'undefined') propagate = true; |
298
|
|
|
|
299
|
|
|
var validationResult = {}; |
300
|
|
|
if (!this.validate(newValue, validationResult)) { |
301
|
|
|
var ErrorClass = validationResult.kind || Error; |
302
|
|
|
throw new ErrorClass(validationResult.message); |
303
|
|
|
} |
304
|
|
|
// trigger a change in the newly selected part to propagate the new index (stored in the part) |
305
|
|
|
// to the backend |
306
|
|
|
this.parts[newValue].setValue(this.parts[newValue].value, propagate); |
307
|
|
|
this.value = newValue; |
308
|
|
|
|
309
|
|
|
// also trigger change on this property (index changed) |
310
|
|
|
this._triggerChange(newValue, this); |
311
|
|
|
|
312
|
|
|
return this; |
313
|
|
|
}, |
314
|
|
|
|
315
|
|
|
toDict: function() { |
316
|
|
|
var obj = {}; |
317
|
|
|
obj[this.name] = { 'value': [this.value, this.parts[this.value].value] }; |
318
|
|
|
|
319
|
|
|
return obj; |
320
|
|
|
}, |
321
|
|
|
|
322
|
|
|
validate: function(value, validationResult) { |
323
|
|
|
if (!isNumber(value) || value % 1 !== 0) { |
324
|
|
|
validationResult.message = 'value must be an integer'; |
325
|
|
|
return false; |
326
|
|
|
} |
327
|
|
|
if (value < 0 || value > this.parts.length) { |
328
|
|
|
validationResult.kind = ValueError; |
329
|
|
|
validationResult.message = 'out of bounds'; |
330
|
|
|
return false; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
return true; |
334
|
|
|
}, |
335
|
|
|
|
336
|
|
|
restoreMirrors: function() { |
337
|
|
|
_.each(this.parts, function(part) { |
338
|
|
|
part.restoreMirrors(); |
339
|
|
|
}); |
340
|
|
|
|
341
|
|
|
this._super(); |
342
|
|
|
}, |
343
|
|
|
|
344
|
|
|
removeAllMirrors: function() { |
345
|
|
|
this._super(); |
346
|
|
|
|
347
|
|
|
_.each(this.parts, function(part) { |
348
|
|
|
part.removeAllMirrors(); |
349
|
|
|
}); |
350
|
|
|
}, |
351
|
|
|
|
352
|
|
|
_sanitize: function() { |
353
|
|
|
var value = typeof this.value === 'undefined' ? this.default : this.value; |
354
|
|
|
|
355
|
|
|
if (!_.isArray(value) && value.length === 2) { |
356
|
|
|
throw new TypeError('expected tuple'); |
357
|
|
|
} |
358
|
|
|
this.value = value[0]; |
359
|
|
|
|
360
|
|
|
if (!_.isArray(this.parts) || this.parts.length < 1) { |
361
|
|
|
throw new ValueError('there must be at least one part'); |
362
|
|
|
} |
363
|
|
|
this._super(); |
364
|
|
|
|
365
|
|
|
return this._setupParts(value[1]); |
366
|
|
|
}, |
367
|
|
|
|
368
|
|
|
_setupParts: function(value) { |
369
|
|
|
var parsedParts = new Array(this.parts.length); |
370
|
|
|
|
371
|
|
|
this.parts = _.each(this.parts, function(part, index) { |
372
|
|
|
var partDef = jQuery.extend({}, part, { |
373
|
|
|
name: this.name, |
374
|
|
|
partInCompound: index, |
375
|
|
|
value: index === this.value ? value : undefined |
376
|
|
|
}); |
377
|
|
|
parsedParts[index] = from(this.owner, this.mirrorers, partDef); |
378
|
|
|
}.bind(this)); |
379
|
|
|
|
380
|
|
|
this.parts = parsedParts; |
381
|
|
|
|
382
|
|
|
return this; |
383
|
|
|
} |
384
|
|
|
}); |
385
|
|
|
|
386
|
|
|
var Epsilon = Property.extend({ |
387
|
|
|
min: -Decimal.MAX_VALUE, |
388
|
|
|
max: Decimal.MAX_VALUE, |
389
|
|
|
step: undefined, |
390
|
|
|
epsilonStep: undefined, |
391
|
|
|
|
392
|
|
|
menuEntryClass: function() { |
393
|
|
|
return PropertyMenuEntry.EpsilonEntry; |
394
|
|
|
}, |
395
|
|
|
|
396
|
|
|
validate: function(value, validationResult) { |
397
|
|
|
if (!_.isArray(value) || value.length != 2) { |
398
|
|
|
validationResult.kind = TypeError; |
399
|
|
|
validationResult.message = 'value must be a tuple'; |
400
|
|
|
return false; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
var center = value[0]; |
404
|
|
|
var epsilon = value[1]; |
405
|
|
|
|
406
|
|
|
// doing a big decimal conversion here due to JavaScripts awesome floating point handling xoxo |
407
|
|
|
var decimalCenter = new Decimal(center); |
408
|
|
|
var decimalEpsilon = new Decimal(epsilon); |
409
|
|
|
|
410
|
|
|
if (typeof center !== 'number' || window.isNaN(center)) { |
411
|
|
|
validationResult.kind = TypeError; |
412
|
|
|
validationResult.message = 'center must be numeric'; |
413
|
|
|
return false; |
414
|
|
|
} else if (typeof epsilon !== 'number' || window.isNaN(epsilon)) { |
415
|
|
|
validationResult.kind = TypeError; |
416
|
|
|
validationResult.message = 'epsilon must be numeric'; |
417
|
|
|
return false; |
418
|
|
|
} else if (epsilon < 0) { |
419
|
|
|
validationResult.kind = ValueError; |
420
|
|
|
validationResult.message = 'epsilon must not be negative'; |
421
|
|
|
return false; |
422
|
|
|
} else if (this.min.gt(decimalCenter.minus(decimalEpsilon)) || this.max.lt(decimalCenter.minus(decimalEpsilon))) { |
423
|
|
|
validationResult.kind = ValueError; |
424
|
|
|
validationResult.message = 'value out of bounds'; |
425
|
|
|
return false; |
426
|
|
|
} else if (typeof this.step !== 'undefined' && !this.default[0].minus(center).mod(this.step).eq(0)) { |
427
|
|
|
validationResult.kind = ValueError; |
428
|
|
|
validationResult.message = 'center not in value range (step)'; |
429
|
|
|
return false; |
430
|
|
|
} else if (typeof this.epsilonStep !== 'undefined' && |
431
|
|
|
!this.default[1].minus(epsilon).mod(this.epsilonStep).eq(0)) { |
432
|
|
|
validationResult.kind = ValueError; |
433
|
|
|
validationResult.message = 'epsilon not in value range (step)'; |
434
|
|
|
return false; |
435
|
|
|
} |
436
|
|
|
return true; |
437
|
|
|
}, |
438
|
|
|
|
439
|
|
|
_sanitize: function() { |
440
|
|
|
if (!_.isArray(this.default) || this.default.length != 2) { |
441
|
|
|
throw new TypeError('tuple', typeof this.default); |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
this.value = typeof this.value === 'undefined' ? this.default.slice(0) : this.value; |
445
|
|
|
|
446
|
|
|
if (!(this.default[0] instanceof Decimal) && isNumber(this.default[0])) { |
447
|
|
|
this.default[0] = new Decimal(this.default[0]); |
448
|
|
|
} else { |
449
|
|
|
throw new TypeError('numeric lower bound', typeof this.default[0]); |
450
|
|
|
} |
451
|
|
|
if (!(this.default[1] instanceof Decimal) && isNumber(this.default[1])) { |
452
|
|
|
this.default[1] = new Decimal(this.default[1]); |
453
|
|
|
} else { |
454
|
|
|
throw new TypeError('numeric upper bound', typeof this.default[1]); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
if (!(this.min instanceof Decimal) && isNumber(this.min)) { |
458
|
|
|
this.min = new Decimal(this.min); |
459
|
|
|
} else { |
460
|
|
|
throw new TypeError('numeric minimum', typeof this.min); |
461
|
|
|
} |
462
|
|
|
if (!(this.max instanceof Decimal) && isNumber(this.max)) { |
463
|
|
|
this.max = new Decimal(this.max); |
464
|
|
|
} else { |
465
|
|
|
throw new TypeError('numeric maximum', typeof this.max); |
466
|
|
|
} |
467
|
|
|
if (typeof this.step !== 'undefined' && !isNumber(this.step)) { |
468
|
|
|
throw new TypeError('numeric step', typeof this.step); |
469
|
|
|
} |
470
|
|
|
if (typeof this.epsilonStep !== 'undefined' && !isNumber(this.epsilonStep)) { |
471
|
|
|
throw new TypeError('numeric epsilon step', typeof this.epsilonStep); |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
if (this.min.gt(this.max)) { |
475
|
|
|
throw new ValueError('bounds violation min/max: ' + this.min + '/' + this.max); |
476
|
|
|
} else if (typeof this.step !== 'undefined' && this.step < 0) { |
477
|
|
|
throw new ValueError('step must be positive, got: ' + this.step); |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
return this._super(); |
481
|
|
|
} |
482
|
|
|
}); |
483
|
|
|
|
484
|
|
|
var Numeric = Property.extend({ |
485
|
|
|
min: -Decimal.MAX_VALUE, |
486
|
|
|
max: Decimal.MAX_VALUE, |
487
|
|
|
step: undefined, |
488
|
|
|
|
489
|
|
|
menuEntryClass: function() { |
490
|
|
|
return PropertyMenuEntry.NumericEntry; |
491
|
|
|
}, |
492
|
|
|
|
493
|
|
|
validate: function(value, validationResult) { |
494
|
|
|
if (!isNumber(value)) { |
495
|
|
|
validationResult.kind = TypeError; |
496
|
|
|
validationResult.message = 'value must be numeric'; |
497
|
|
|
return false; |
498
|
|
|
} else if (this.min.gt(value) || this.max.lt(value)) { |
499
|
|
|
validationResult.kind = ValueError; |
500
|
|
|
validationResult.message = 'value out of bounds'; |
501
|
|
|
return false; |
502
|
|
|
} else if (typeof this.step !== 'undefined' && !this.default.minus(value).mod(this.step).eq(0)) { |
503
|
|
|
validationResult.kind = ValueError; |
504
|
|
|
validationResult.message = 'value not in value range (step)'; |
505
|
|
|
return false; |
506
|
|
|
} |
507
|
|
|
return true; |
508
|
|
|
}, |
509
|
|
|
|
510
|
|
|
_sanitize: function() { |
511
|
|
|
this.value = typeof this.value === 'undefined' ? this.default : this.value; |
512
|
|
|
|
513
|
|
|
if (isNumber(this.default)) { |
514
|
|
|
this.default = new Decimal(this.default); |
515
|
|
|
} else { |
516
|
|
|
throw new TypeError('numeric default', this.default); |
517
|
|
|
} |
518
|
|
|
if (isNumber(this.min)) { |
519
|
|
|
this.min = new Decimal(this.min); |
520
|
|
|
} else { |
521
|
|
|
throw new TypeError('numeric min', this.min); |
522
|
|
|
} |
523
|
|
|
if (isNumber(this.max)) { |
524
|
|
|
this.max = new Decimal(this.max); |
525
|
|
|
} else { |
526
|
|
|
throw new TypeError('numeric max', this.max); |
527
|
|
|
} |
528
|
|
|
if (typeof this.step !== 'undefined' && !isNumber(this.step)) { |
529
|
|
|
throw new TypeError('numeric step', this.step); |
530
|
|
|
} |
531
|
|
|
|
532
|
|
|
if (this.min.gt(this.max)) { |
533
|
|
|
throw new ValueError('bounds violation min/max: ' + this.min + '/' + this.max); |
534
|
|
|
} else if (typeof this.step !== 'undefined' && this.step < 0) { |
535
|
|
|
throw new ValueError('step must be positive, got: ' + this.step); |
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
return this._super(); |
539
|
|
|
} |
540
|
|
|
}); |
541
|
|
|
|
542
|
|
|
var Range = Property.extend({ |
543
|
|
|
min: -Decimal.MAX_VALUE, |
544
|
|
|
max: Decimal.MAX_VALUE, |
545
|
|
|
step: undefined, |
546
|
|
|
|
547
|
|
|
menuEntryClass: function() { |
548
|
|
|
return PropertyMenuEntry.RangeEntry; |
549
|
|
|
}, |
550
|
|
|
|
551
|
|
|
validate: function(value, validationResult) { |
552
|
|
|
if (!_.isArray(this.value) || this.value.length != 2) { |
553
|
|
|
validationResult.kind = TypeError; |
554
|
|
|
validationResult.message = 'value must be a tuple'; |
555
|
|
|
return false; |
556
|
|
|
} |
557
|
|
|
|
558
|
|
|
var lower = value[0]; |
559
|
|
|
var upper = value[1]; |
560
|
|
|
if (!isNumber(lower) || !isNumber(upper)) { |
561
|
|
|
validationResult.kind = TypeError; |
562
|
|
|
validationResult.message = 'lower and upper bound must be numeric'; |
563
|
|
|
return false; |
564
|
|
|
} else if (lower > upper) { |
565
|
|
|
validationResult.kind = ValueError; |
566
|
|
|
validationResult.message = 'lower bound must be less or equal upper bound'; |
567
|
|
|
return false; |
568
|
|
|
} else if (typeof this.step !== 'undefined' && !this.default[0].minus(lower).mod(this.step).eq(0) || |
569
|
|
|
!this.default[1].minus(upper).mod(this.step).eq(0)) { |
570
|
|
|
validationResult.kind = ValueError; |
571
|
|
|
validationResult.message = 'value not in value range (step)'; |
572
|
|
|
return false; |
573
|
|
|
} |
574
|
|
|
return true; |
575
|
|
|
}, |
576
|
|
|
|
577
|
|
|
_sanitize: function() { |
578
|
|
|
|
579
|
|
|
if (!_.isArray(this.default) || this.default.length != 2) { |
580
|
|
|
throw new TypeError('tuple', this.default); |
581
|
|
|
} |
582
|
|
|
|
583
|
|
|
this.value = typeof this.value === 'undefined' ? this.default.slice(0) : this.value; |
584
|
|
|
|
585
|
|
|
if (!(this.default[0] instanceof Decimal) && isNumber(this.default[0])) { |
586
|
|
|
this.default[0] = new Decimal(this.default[0]); |
587
|
|
|
} else { |
588
|
|
|
throw new TypeError('numeric default lower bound', this.default[0]); |
589
|
|
|
} |
590
|
|
|
if (!(this.default[1] instanceof Decimal) && isNumber(this.default[1])) { |
591
|
|
|
this.default[1] = new Decimal(this.default[1]); |
592
|
|
|
} else { |
593
|
|
|
throw new TypeError('numeric default upper bound', this.default[1]); |
594
|
|
|
} |
595
|
|
|
|
596
|
|
|
if (!(this.min instanceof Decimal) && isNumber(this.min)) { |
597
|
|
|
this.min = new Decimal(this.min); |
598
|
|
|
} else { |
599
|
|
|
throw new TypeError('numeric min', this.min); |
600
|
|
|
} |
601
|
|
|
if (!(this.max instanceof Decimal) && isNumber(this.max)) { |
602
|
|
|
this.max = new Decimal(this.max); |
603
|
|
|
} else { |
604
|
|
|
throw new TypeError('numeric max', this.max); |
605
|
|
|
} |
606
|
|
|
if (typeof this.step !== 'undefined' && !isNumber(this.step)) { |
607
|
|
|
throw new ValueError('numeric step', this.step); |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
if (this.min.gt(this.max)) { |
611
|
|
|
throw new ValueError('bounds violation min/max: ' + this.min + '/' + this.max); |
612
|
|
|
} else if (typeof this.step !== 'undefined' && this.step < 0) { |
613
|
|
|
throw new ValueError('step must be positive: ' + this.step); |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
return this._super(); |
617
|
|
|
} |
618
|
|
|
}); |
619
|
|
|
|
620
|
|
|
var Text = Property.extend({ |
621
|
|
|
notEmpty: false, |
622
|
|
|
|
623
|
|
|
menuEntryClass: function() { |
624
|
|
|
return PropertyMenuEntry.TextEntry; |
625
|
|
|
}, |
626
|
|
|
|
627
|
|
|
validate: function(value, validationResult) { |
628
|
|
|
if (this.notEmpty && value === '') { |
629
|
|
|
validationResult.kind = ValueError; |
630
|
|
|
validationResult.message = 'value must not be empty'; |
631
|
|
|
return false; |
632
|
|
|
} |
633
|
|
|
return true; |
634
|
|
|
}, |
635
|
|
|
|
636
|
|
|
_sanitize: function() { |
637
|
|
|
this.value = typeof this.value === 'undefined' ? this.default : String(this.value); |
638
|
|
|
return this._super(); |
639
|
|
|
} |
640
|
|
|
}); |
641
|
|
|
|
642
|
|
|
var InlineTextField = Text.extend({ |
643
|
|
|
menuEntryClass: function() { |
644
|
|
|
return PropertyMenuEntry.InlineTextArea; |
645
|
|
|
}, |
646
|
|
|
|
647
|
|
|
validate : function(value, validationResult) { |
648
|
|
|
return true; |
649
|
|
|
} |
650
|
|
|
}); |
651
|
|
|
|
652
|
|
|
var Transfer = Property.extend({ |
653
|
|
|
UNLINK_VALUE: -1, |
654
|
|
|
UNLINK_TEXT: 'unlinked', |
655
|
|
|
GRAPHS_URL: Factory.getModule('Config').Backend.BASE_URL + Factory.getModule('Config').Backend.GRAPHS_URL + '/', |
656
|
|
|
|
657
|
|
|
transferGraphs: undefined, |
658
|
|
|
|
659
|
|
|
init: function(owner, mirrorers, definition) { |
660
|
|
|
jQuery.extend(this, definition); |
661
|
|
|
this.owner = owner; |
662
|
|
|
this._sanitize() |
663
|
|
|
._setupMirrors() |
664
|
|
|
._setupMenuEntry() |
665
|
|
|
.fetchTransferGraphs(); |
666
|
|
|
}, |
667
|
|
|
|
668
|
|
|
menuEntryClass: function() { |
669
|
|
|
return PropertyMenuEntry.TransferEntry; |
670
|
|
|
}, |
671
|
|
|
|
672
|
|
|
validate: function(value, validationResult) { |
673
|
|
|
if (value === this.UNLINK_VALUE) { |
674
|
|
|
validationResult.kind = Warning; |
675
|
|
|
validationResult.message = 'no link set'; |
676
|
|
|
} else if (!_.has(this.transferGraphs, value)) { |
677
|
|
|
validationResult.kind = ValueError; |
678
|
|
|
validationResult.message = 'specified graph unknown'; |
679
|
|
|
return false; |
680
|
|
|
} |
681
|
|
|
|
682
|
|
|
return true; |
683
|
|
|
}, |
684
|
|
|
|
685
|
|
|
_sanitize: function() { |
686
|
|
|
// do not validate |
687
|
|
|
this.value = typeof this.value === 'undefined' ? this.default : this.value; |
688
|
|
|
return this; |
689
|
|
|
}, |
690
|
|
|
|
691
|
|
|
_triggerChange: function(value, issuer) { |
692
|
|
|
var unlinked = value === this.UNLINK_VALUE; |
693
|
|
|
|
694
|
|
|
if (!unlinked) this.owner.hideBadge(); |
695
|
|
|
jQuery(this).trigger(Factory.getModule('Config').Events.NODE_PROPERTY_CHANGED, [ |
696
|
|
|
value, |
697
|
|
|
unlinked ? this.UNLINK_TEXT : this.transferGraphs[value], |
698
|
|
|
issuer] |
699
|
|
|
); |
700
|
|
|
}, |
701
|
|
|
|
702
|
|
|
fetchTransferGraphs: function() { |
703
|
|
|
jQuery.ajax({ |
704
|
|
|
url: this.GRAPHS_URL + '?kind=' + this.owner.graph.kind, |
705
|
|
|
type: 'GET', |
706
|
|
|
dataType: 'json', |
707
|
|
|
// don't show progress |
708
|
|
|
global: false, |
709
|
|
|
|
710
|
|
|
success: this._setTransferGraphs.bind(this), |
711
|
|
|
error: this._throwError |
712
|
|
|
}); |
713
|
|
|
}, |
714
|
|
|
|
715
|
|
|
_setTransferGraphs: function(json) { |
716
|
|
|
this.transferGraphs = _.reduce(json.graphs, function(all, current) { |
717
|
|
|
var id = window.parseInt(_.last(current.url.split('/'))); |
718
|
|
|
all[id] = current.name; |
719
|
|
|
return all; |
720
|
|
|
}, {}); |
721
|
|
|
delete this.transferGraphs[this.owner.graph.id]; |
722
|
|
|
|
723
|
|
|
if (this.value === this.UNLINK_VALUE) |
724
|
|
|
this.owner.showBadge('!', 'important'); |
725
|
|
|
|
726
|
|
|
jQuery(this).trigger(Factory.getModule('Config').Events.PROPERTY_SYNCHRONIZED); |
727
|
|
|
this._triggerChange(this.value, this); |
728
|
|
|
|
729
|
|
|
return this; |
730
|
|
|
}, |
731
|
|
|
|
732
|
|
|
_throwError: function(xhr, textStatus, errorThrown) { |
733
|
|
|
Alerts.showWarningAlert('Could not fetch graph for transfer:', errorThrown, Factory.getModule('Config').Alerts.TIMEOUT); |
734
|
|
|
|
735
|
|
|
this.value = this.UNLINK_VALUE; |
736
|
|
|
this.transferGraphs = undefined; |
737
|
|
|
|
738
|
|
|
jQuery(this).trigger(Factory.getModule('Config').Events.PROPERTY_SYNCHRONIZED); |
739
|
|
|
} |
740
|
|
|
}); |
741
|
|
|
|
742
|
|
|
var from = function(owner, mirrorers, definition) { |
743
|
|
|
switch (definition.kind) { |
744
|
|
|
case 'bool': return new Bool(owner, mirrorers, definition); |
745
|
|
|
case 'choice': return new Choice(owner, mirrorers, definition); |
746
|
|
|
case 'compound': return new Compound(owner, mirrorers, definition); |
747
|
|
|
case 'epsilon': return new Epsilon(owner, mirrorers, definition); |
748
|
|
|
case 'numeric': return new Numeric(owner, mirrorers, definition); |
749
|
|
|
case 'range': return new Range(owner, mirrorers, definition); |
750
|
|
|
case 'text': return new Text(owner, mirrorers, definition); |
751
|
|
|
case 'textfield':return new InlineTextField(owner, mirrorers, definition); |
752
|
|
|
case 'transfer': return new Transfer(owner, mirrorers, definition); |
753
|
|
|
|
754
|
|
|
default: throw ValueError('unknown property kind ' + definition.kind); |
755
|
|
|
} |
756
|
|
|
}; |
757
|
|
|
|
758
|
|
|
return { |
759
|
|
|
Bool: Bool, |
760
|
|
|
Choice: Choice, |
761
|
|
|
Compound: Compound, |
762
|
|
|
Epsilon: Epsilon, |
763
|
|
|
Numeric: Numeric, |
764
|
|
|
Property: Property, |
765
|
|
|
Range: Range, |
766
|
|
|
Text: Text, |
767
|
|
|
InlineTextField: InlineTextField, |
768
|
|
|
Transfer: Transfer, |
769
|
|
|
from: from |
770
|
|
|
}; |
771
|
|
|
}); |
772
|
|
|
|