Passed
Push — master ( 1713a6...bcb549 )
by Peter
02:05
created

FuzzEd/static/script/property.js   F

Complexity

Total Complexity 196
Complexity/F 2.97

Size

Lines of Code 771
Function Count 66

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 0
dl 0
loc 771
rs 2.1818
c 1
b 0
f 0
wmc 196
nc 0
mnd 6
bc 140
fnc 66
bpm 2.1212
cpm 2.9695
noi 41

How to fix   Complexity   

Complexity

Complex classes like FuzzEd/static/script/property.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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