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

FuzzEd/static/script/property_menu_entry.js   F

Complexity

Total Complexity 138
Complexity/F 1.53

Size

Lines of Code 1296
Function Count 90

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
dl 0
loc 1296
rs 2.4
c 0
b 0
f 0
wmc 138
nc 1048576
mnd 2
bc 110
fnc 90
bpm 1.2222
cpm 1.5333
noi 25

How to fix   Complexity   

Complexity

Complex classes like FuzzEd/static/script/property_menu_entry.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', 'jquery'], function(Factory, Class, Config) {
2
    /**
3
     * Package: Base
4
     */
5
6
    /**
7
     * Constants:
8
     *      {RegEx} NUMBER_REGEX - RegEx for matching all kind of number representations with strings.
9
     */
10
    var NUMBER_REGEX = /^[+\-]?(?:0|[1-9]\d*)(?:[.,]\d*)?(?:[eE][+\-]?\d+)?$/;
11
12
    /**
13
     * Function: capitalize
14
     *      Helper function for capitalizing the first letter of a string.
15
     *
16
     * Returns:
17
     *      {String} capitalized - Capitalized copy of the string.
18
     */
19
    var capitalize = function(aString) {
20
        return aString.charAt(0).toUpperCase() + aString.slice(1);
21
    };
22
23
    /**
24
     * Function: escape
25
     *      HTML escapes the provided text to only contain break tags instead of linebreaks.
26
     *
27
     * Returns:
28
     *      {String} escaped - The HTML escaped version of the string.
29
     */
30
	var escape = function(aString) {
31
		return _.escape(aString).replace(/\n/g, '<br>');
32
	};
33
	
34
    /**
35
     * Abstract Class: Entry
36
     *      Abstract base class for an entry in the property menu of a node. It's associated with a <Property> object
37
     *      and handles the synchronization with it.
38
     */
39
    var Entry = Class.extend({
40
        /**
41
         * Group: Members
42
         *      {String}     id            - Form element ID for value retrieval.
43
         *      {<Property>} property      - The associated <Property> object.
44
         *      {DOMElement} container     - The container element in the property dialog.
45
         *      {DOMElement} inputs        - A selector containing all relevant form elements.
46
         *
47
         *      {Boolean}    _editing      - A flag that marks this entry as currently beeing edited.
48
         *      {Object}     _preEditValue - The last valid value stored before editing this entry.
49
         *      {DOMElement} _editTarget   - A selector containing the one form element that is being edited.
50
         *      {Timeout}    _timer        - The Timeout object used to prevent updates from firing immediately.
51
         */
52
        id:            undefined,
53
        property:      undefined,
54
        container:     undefined,
55
        inputs:        undefined,
56
57
        _editing:      false,
58
        _preEditValue: undefined,
59
        _editTarget:   undefined,
60
        _timer:        undefined,
61
62
        /**
63
         * Constructor: init
64
         *      Initializes the menu entry. Sets up the node's visual representation, event handler and state during the
65
         *      process.
66
         *
67
         *  Parameters:
68
         *      {<Property>} property - The associated <Property> object.
69
         */
70
        init: function(property) {
71
            this.id       = _.uniqueId('property');
72
            this.property = property;
73
74
            this._setupVisualRepresentation()
75
                ._setupEvents();
76
        },
77
78
        /**
79
         * Section: Event Handling
80
         */
81
82
        /**
83
         * Method: blurEvents
84
         *      States the blur (think: 'stop editing') events this Entry should react on.
85
         *
86
         * Returns:
87
         *      {Array[String]} - Array of blury events.
88
         */
89
        blurEvents: function() {
90
            return ['blur', 'remove'];
91
        },
92
93
        /**
94
         * Method: blurred
95
         *      Callback method that gets fired when one of the blur events specified in <blurEvents> was fired. Takes
96
         *      care of validation and propagating the new value of the Entry to the associated <Property>. If the new
97
         *      value is not valid for the property, its old value will be restored.
98
         *
99
         * Parameters:
100
         *      See jQuery event callbacks.
101
         *
102
         * Returns:
103
         *      This {Entry} for chaining.
104
         */
105
        blurred: function(event, ui) {
106
            if (!this._editing) {
107
                this._preEditValue = this.property.value;
108
            }
109
110
            this.fix(event, ui);
111
            this._abortChange().unwarn();
112
113
            if (this.property.validate(this._value(), {})) {
114
                this.property.setValue(this._value(), this);
115
            } else {
116
                this._value(this._preEditValue);
117
                this.property.setValue(this._preEditValue, this, false);
118
            }
119
120
            this._editing      = false;
121
            this._editTarget   = undefined;
122
            this._preEditValue = undefined;
123
124
            return this;
125
        },
126
127
        /**
128
         * Method: changeEvents
129
         *      Return the change ('currently editing') events this Entry should react on.
130
         *
131
         *  Returns:
132
         *      {Array[String]} - Array of change event names.
133
         */
134
        changeEvents: function() {
135
            return [];
136
        },
137
138
        /**
139
         * Method: changed
140
         *      Callback method that gets fired when one of the change events specified in <changeEvents> was fired.
141
         *      Takes care of validation and propagating the new value of the Entry to the associated <Property>. If the
142
         *      new value is not valid it will display an appropriate error message.  Valid values will be propagated to
143
         *      the <Property> after a short timeout to prevent propagation while changing the value too often.
144
         *
145
         * Parameters:
146
         *      See jQuery event callbacks.
147
         *
148
         * Returns:
149
         *      This {Entry} for chaining.
150
         */
151
        changed: function(event, ui) {
152
            if (!this._editing) {
153
                this._preEditValue = this.property.value;
154
            }
155
156
            var validationResult = {};
157
            this._editing    = true;
158
            this._editTarget = event.target;
159
160
            this.fix(event, ui);
161
162
            if (this.property.validate(this._value(), validationResult)) {
163
                this._sendChange().unwarn();
164
            } else {
165
                this._abortChange().warn(validationResult.message);
166
            }
167
168
            return this;
169
        },
170
171
        /**
172
         * Method: _abortChange
173
         *      Abort the currently running value propagation timeout to prevent the propagation of the value to the
174
         *      <Property>.
175
         *
176
         * Returns:
177
         *      This {Entry} for chaining.
178
         */
179
        _abortChange: function() {
180
            window.clearTimeout(this._timer);
181
182
            return this;
183
        },
184
185
        /**
186
         * Method: _sendChange
187
         *      Propagate the currently set value to the <Property> object after a short timeout (to prevent in-to-deep-
188
         *      propagation). If there is already a timeout running it will cancel that timeout and start a new one.
189
         *
190
         * Returns:
191
         *      This {Entry} for chaining.
192
         */
193
        _sendChange: function() {
194
            // discard old timeout
195
            window.clearTimeout(this._timer);
196
            var value = this._value();
197
            // create a new one
198
            this._timer = window.setTimeout(function() {
199
                this.property.setValue(value, this);
200
            }.bind(this), Factory.getModule('Config').Menus.PROPERTIES_MENU_TIMEOUT);
201
202
            return this;
203
        },
204
205
        /**
206
         *  Section: Validation
207
         */
208
209
        /**
210
         * Method: fix
211
         *      This method allows for "fixing" the value in the menu entries visual input before passing it to the
212
         *      properties validate method. This allows for example neat user interface convenience features like
213
         *      raising the upper boundary of an interval input when the lower boundary gets larger.
214
         *
215
         * Returns:
216
         *      This {Entry} for chaining.
217
         */
218
        fix: function(event, ui) {
219
            return this;
220
        },
221
222
        /**
223
         *  Section: Visuals
224
         */
225
226
        /**
227
         * Method: appendTo
228
         *      Adds this Entry to the container in the properties menu.
229
         *
230
         * Parameters:
231
         *      {jQuery Selector} on - The element this Entry should be appended to.
232
         *
233
         * Returns:
234
         *      This {Entry} for chaining.
235
         */
236
        appendTo: function(on) {
237
            on.append(this.container);
238
            this._setupCallbacks();
239
240
            return this;
241
        },
242
243
        /**
244
         * Method: insertAfter
245
         *      Adds this Entry after another element to the properties menu.
246
         *
247
         * Parameters:
248
         *      {jQuery Selector} element - The element this Entry should be inserted after.
249
         *
250
         * Returns:
251
         *      This {Entry} for chaining.
252
         */
253
        insertAfter: function(element) {
254
            element.after(this.container);
255
            this._setupCallbacks();
256
257
            return this;
258
        },
259
260
        /**
261
         * Method: remove
262
         *      Removes this Entry to the container in the properties menu.
263
         *
264
         * Returns:
265
         *      This {Entry} for chaining.
266
         */
267
        remove: function() {
268
            this.container.remove();
269
270
            return this;
271
        },
272
273
        /**
274
         * Method: setReadonly
275
         *      Marks the menu entry as readonly. Cannot be modified but any longer. However, visualization and copying
276
         *      of the value is still legal.
277
         *
278
         * Parameters:
279
         *      {Boolean} readonly - The new readonly state to set for this entry.
280
         *
281
         * Returns:
282
         *      This {Entry} for chaining.
283
         */
284
        setReadonly: function(readonly) {
285
            this.inputs
286
                .attr('readonly', readonly ? 'readonly' : null)
287
                .toggleClass('disabled', readonly);
288
289
            return this;
290
        },
291
292
        /**
293
         * Method: setHidden
294
         *      Sets the hidden state of this menu entry. Hidden entries do not appear in the property menu.
295
         *
296
         * Parameters:
297
         *      {Boolean} hidden - The new hidden state to set for this entry.
298
         *
299
         * Returns:
300
         *      This {Entry} for chaining.
301
         */
302
        setHidden: function(hidden) {
303
            this.container.toggle(!hidden);
304
305
            return this;
306
        },
307
308
        /**
309
         * Method: warn
310
         *      Highlight the corresponding form elements (error state) and show a popup containing an error message.
311
         *
312
         * Returns:
313
         *      This {Entry} for chaining.
314
         */
315
        warn: function(text) {
316
            if (this.container.hasClass(Factory.getModule('Config').Classes.PROPERTY_WARNING) &&
317
                this.container.attr('data-original-title') === text)
318
                return this;
319
320
            this.container
321
                .addClass(Factory.getModule('Config').Classes.PROPERTY_WARNING)
322
                .attr('data-original-title', text)
323
                .tooltip('show');
324
325
            return this;
326
        },
327
328
        /**
329
         * Method: unwarn
330
         *      Restores the normal state of all form elements and hides warning popups.
331
         *
332
         * Returns:
333
         *      This {Entry} for chaining.
334
         */
335
        unwarn: function() {
336
            this.container.removeClass(Factory.getModule('Config').Classes.PROPERTY_WARNING).tooltip('hide');
337
338
            return this;
339
        },
340
341
        /**
342
         *  Section: Setup
343
         */
344
345
        /**
346
         * Method: _setupVisualRepresentation
347
         *      Sets up all visuals (container and inputs).
348
         *
349
         * Returns:
350
         *      This {Entry} for chaining.
351
         */
352
        _setupVisualRepresentation: function() {
353
            this._setupContainer()
354
                ._setupInput();
355
            this.container.find('.inputs').prepend(this.inputs);
356
357
            this.setReadonly(this.property.readonly);
358
            this.setHidden(this.property.hidden);
359
360
            return this;
361
        },
362
363
        /**
364
         * Method: _setupContainer
365
         *      Sets up the container element.
366
         *
367
         * Returns:
368
         *      This {Entry} for chaining.
369
         */
370
        _setupContainer: function() {
371
            this.container = jQuery(
372
                '<div class="form-group" data-toggle="tooltip" data-trigger="manual" data-placement="left">\
373
                    <label class="col-4 control-label" for="' + this.id + '">' + (this.property.displayName || '') + '</label>\
374
                    <div class="inputs col-8"></div>\
375
                </div>'
376
            );
377
378
            return this;
379
        },
380
381
        /**
382
         *  Abstract Method: _setupInput
383
         *      Sets up all needed input (form) elements. Could be e.g. a text input, checkbox, ... Must be implemented
384
         *      by a subclass.
385
         */
386
        _setupInput: function() {
387
            throw SubclassResponsibility();
388
        },
389
390
        /**
391
         * Method: _setupCallbacks
392
         *      Sets up the callbacks for change and blur events on input elements.
393
         *
394
         * Returns:
395
         *      This {Entry} for chaining.
396
         */
397
        _setupCallbacks: function() {
398
            _.each(this.blurEvents(), function(event) {
399
                this.inputs.on(event, this.blurred.bind(this));
400
            }.bind(this));
401
402
            _.each(this.changeEvents(), function(event) {
403
                this.inputs.on(event, this.changed.bind(this));
404
            }.bind(this));
405
406
            return this;
407
        },
408
409
        /**
410
         * Method: _setupEvents
411
         *      Register for changes of the associated <Property> object.
412
         *
413
         * Returns:
414
         *      This {Entry} for chaining.
415
         */
416
        _setupEvents: function() {
417
            jQuery(this.property).on([
418
                Factory.getModule('Config').Events.NODE_PROPERTY_CHANGED,
419
                Factory.getModule('Config').Events.EDGE_PROPERTY_CHANGED,
420
                Factory.getModule('Config').Events.NODEGROUP_PROPERTY_CHANGED
421
            ].join(' '), function(event, newValue, text, issuer) {
422
                // ignore changes issued by us in order to prevent race conditions with the user
423
                if (issuer === this) return;
424
                this._value(newValue);
425
            }.bind(this));
426
427
428
            jQuery(this.property).on(Factory.getModule('Config').Events.PROPERTY_READONLY_CHANGED, function(event, newReadonly) {
429
                this.setReadonly(newReadonly);
430
            }.bind(this));
431
432
            jQuery(this.property).on(Factory.getModule('Config').Events.PROPERTY_HIDDEN_CHANGED, function(event, newHidden) {
433
                this.setHidden(newHidden);
434
            }.bind(this));
435
436
            return this;
437
        },
438
439
        /**
440
         *  Section: Accessors
441
         */
442
443
        /**
444
         * Method: _value
445
         *      Method used for retrieving the current property value from the inputs.
446
         */
447
        _value: function(newValue) {
448
            throw SubclassResponsibility();
449
        }
450
    });
451
452
    /**
453
     * Class: BoolEntry
454
     *      Concrete <Entry> implementation that represents a bool property. Visual representation used is a checkbox.
455
     */
456
    var BoolEntry = Entry.extend({
457
        /**
458
         * Method: blurEvents
459
         *      Override, checkboxes do not fire blur events.
460
         *
461
         * Returns:
462
         *      {Array[String]} - List of change event names.
463
         */
464
        blurEvents: function() { return ['change']; },
465
466
        /**
467
         * Method: setReadonly
468
         *      Override due to the fact that checkbox require different HTML attribute to be set.
469
         *
470
         * Returns:
471
         *      This {BoolEntry} for chaining.
472
         */
473
        setReadonly: function(readonly) {
474
            this.inputs.attr('disabled', readonly ? 'disabled' : null);
475
476
            return this._super(readonly);
477
        },
478
479
        /**
480
         * Method: _setupInput
481
         *      Concrete implementation of the method. Returns HTML markup for a checkbox.
482
         *
483
         * Returns:
484
         *      This {BoolEntry} for chaining.
485
         */
486
        _setupInput: function() {
487
            this.inputs = jQuery('<input type="checkbox">')
488
                .attr('id', this.id);
489
490
            return this;
491
        },
492
493
        /**
494
         * Method: _value
495
         *      Concrete implementation of _value. Returns the checked attribute state of the checkbox as value.
496
         *
497
         * Returns:
498
         *      {BoolEntry} this    - For chaining when used as setter.
499
         *      {Boolean}   checked - The entries value when used as a getter.
500
         *
501
         */
502
        _value: function(newValue) {
503
            if (typeof newValue === 'undefined') return this.inputs.is(':checked');
504
            this.inputs.attr('checked', newValue ? 'checked' : null);
505
506
            return this;
507
        }
508
    });
509
510
    /**
511
     * Class: ChoiceEntry
512
     *      An Entry allowing to select a value from a list of values defined by a <Property::Choice>. Visualized by an
513
     *      HTML select element.
514
     */
515
    var ChoiceEntry = Entry.extend({
516
        /**
517
         * Method: blurEvents
518
         *      Overrides standard collection of blur events. Additionally contains the change event that is specific
519
         *      for select elements
520
         *
521
         * Returns:
522
         *      {Array[String]} changeEvents - The blur events.
523
         */
524
        blurEvents: function() {
525
            return ['blur', 'change', 'remove'];
526
        },
527
528
        /**
529
         * Method: setReadonly
530
         *      Overrides standard readonly setter. Select elements need to set the HTML disabled attribute in order to
531
         *      be readonly. Setting the readonly attribute is not sufficient.
532
         *
533
         * Parameters:
534
         *      {Boolean} readonly - the readonly state as boolean.
535
         *
536
         * Returns:
537
         *      This {ChoiceEntry} for chaining.
538
         */
539
        setReadonly: function(readonly) {
540
            this.inputs.attr('disabled', readonly ? 'disabled' : null);
541
542
            return this._super(readonly);
543
        },
544
545
        /**
546
         * Method: _setupInput
547
         *      The choice input element is represented by an HTML select element. This method constructs it and stores
548
         *      it in the _input member. The preselected option of the select is either given in the notation as default
549
         *      value or is none.
550
         *
551
         * Returns:
552
         *      This {ChoiceEntry} for chaining.
553
         */
554
        _setupInput: function() {
555
            var value    = this.property.value;
556
            this.inputs  = jQuery('<select class="form-control input-small">').attr('id', this.id);
557
558
            var selected = this.property.choices[this._indexForValue(value)];
559
560
            _.each(this.property.choices, function(choice, index) {
561
                this.inputs.append(jQuery('<option>')
562
                    .text(choice)
563
                    .val(index)
564
                    .attr('selected', choice === selected ? 'selected' : null)
565
                )
566
            }.bind(this));
567
568
            return this;
569
        },
570
571
        /**
572
         * Method: _indexForValue
573
         *      Reverse search of a index belonging to a given value.
574
         *
575
         * Parameters:
576
         *      {Object} value - The value of an entry.
577
         *
578
         * Returns:
579
         *      The {Number} index of the given value. -1 if the lookup failed.
580
         */
581
        _indexForValue: function(value) {
582
            for (var i = this.property.values.length -1; i >= 0; i--) {
583
                if (_.isEqual(this.property.values[i], value)) {
584
                    return i;
585
                }
586
            }
587
588
            return -1;
589
        },
590
591
        /**
592
         * Method: _value
593
         *      Concrete implementation of the _value method. Returns the value of the currently selected option of the
594
         *      select element if the method's parameter is unset. Otherwise the passed will be set. The lookup of the
595
         *      value is done as in <_indexForValue>.
596
         *
597
         * Parameters:
598
         *      {String} newValue - [optional] optional new value of the choice entry.
599
         *
600
         * Returns:
601
         *      This {ChoiceEntry} for chaining.
602
         */
603
        _value: function(newValue) {
604
            if (typeof newValue === 'undefined') {
605
                return this.property.values[this.inputs.val()];
606
            }
607
            this.inputs.val(this._indexForValue(newValue));
608
609
            return this;
610
        }
611
    });
612
613
    /**
614
     *  Class: CompoundEntry
615
     *      A container entry containing multiple other Entries. This is the graphical equivalent to a <Compound>
616
     *      <Property>. The active child Property can be chosen with radio buttons. The CompoundEntry ensures the
617
     *      consistency of updates with the backend.
618
     */
619
    var CompoundEntry = Entry.extend({
620
        blurEvents: function() {
621
            return ['click'];
622
        },
623
624
        appendTo: function(on) {
625
            this._super(on);
626
            _.each(this.property.parts, function(part, index) {
627
                part.menuEntry.insertAfter(this.container);
628
                // child entries should not update on remove because only visible entries should be allowed
629
                // to propagate their value which is ensured by the parent compound (on remove)
630
                part.menuEntry.inputs.off('remove');
631
                part.menuEntry.setHidden(this.property.value !== index);
632
            }.bind(this));
633
634
            return this;
635
        },
636
637
        remove: function() {
638
            _.each(this.property.parts, function(part, index) {
639
                part.menuEntry.remove();
640
            });
641
642
            return this._super();
643
        },
644
645
        setReadonly: function(readonly) {
646
            this.inputs.attr('disabled', readonly ? 'disabled' : null);
647
648
            return this._super(readonly);
649
        },
650
651
        _setupInput: function() {
652
            this.inputs = jQuery('<div class="btn-group" data-toggle="buttons">');
653
654
            _.each(this.property.parts, function(part, index) {
655
                var buttonLabel = jQuery('<label class="btn btn-default btn-small"></label>')
656
                var button = jQuery('<input type="radio">');
657
                buttonLabel.text(part.partName);
658
                button.attr('active', index === this.property.value ? 'active' : '');
659
                buttonLabel.append(button);
660
                this.inputs.append(buttonLabel);
661
            }.bind(this));
662
        },
663
664
        _value: function(newValue) {
665
            if (typeof newValue === 'undefined') {
666
                return this.inputs.find('.active').index();
667
            }
668
669
            this.inputs.find('.btn:nth-child(' + (newValue + 1) + ')').button('toggle');
670
            this._showPartAtIndex(newValue);
671
        },
672
673
        fix: function(event, ui) {
674
            // we try to read the button state before bootstraps event handling is finished, so we
675
            // force bootstrap to manually toggle the button state before we read it
676
            jQuery(event.target).button('toggle');
677
        },
678
679
        /**
680
         *  Method: _showPartAtIndex
681
         *      Make the child Entry with the given index visible and hide all others.
682
         *
683
         *  Parameters:
684
         *      {Integer} index - The index of the child Entry that should be displayed.
685
         *
686
         *  Returns:
687
         *      This Entry instance for chaining.
688
         */
689
        _showPartAtIndex: function(index) {
690
            _.each(this.property.parts, function(part, iterIndex) {
691
                part.setHidden(iterIndex !== index);
692
            });
693
694
            return this;
695
        }
696
    });
697
698
    /**
699
     * Class: NumericEntry
700
     *      Input field for a <Property::Numeric>. It ensures that only number-typed values are allowed and provides
701
     *      convenience functions like stepping with spinners. It is concrete implementation of <PropertyMenuEntry>.
702
     */
703
    var NumericEntry = Entry.extend({
704
        /**
705
         * Method: blurEvents
706
         *      Overrides the standard list of blur events. Additionally contains change in order to support HTML 5
707
         *      spinners.
708
         *
709
         * Returns:
710
         *      {Array[String]} blurEvents - The blur events.
711
         */
712
        blurEvents: function() {
713
            return ['blur', 'change', 'remove'];
714
        },
715
716
        /**
717
         * Method: changeEvents
718
         *      Overrides the standard list of change events. Number input fields behave like normal text fields and
719
         *      therefore need to support changes on key presses, cuts and pasts.
720
         *
721
         * Returns:
722
         *      {Array[String]} changeEvent - the change events.
723
         */
724
        changeEvents: function() {
725
            return ['keyup', 'cut', 'paste'];
726
        },
727
728
        /**
729
         * Method: _setupInput
730
         *      Concrete implementation of the _setupInput method. Returns a HTML 5 number input field. The method sets
731
         *      the respective attributes for min/max/step of the number field. If a browser does not support HTML 5 it
732
         *      will be interpreted as a text input instead.
733
         *
734
         * Returns:
735
         *      This {NumberEntry} for chaining.
736
         */
737
        _setupInput: function() {
738
            this.inputs = jQuery('<input type="number" class="form-control input-small">')
739
                .attr('id',   this.id)
740
                .attr('min',  this.property.min)
741
                .attr('max',  this.property.max)
742
                .attr('step', this.property.step);
743
744
            return this;
745
        },
746
747
        /**
748
         * Method: _value
749
         *      Concrete implementation of this method. Functions as getter and setter. If no new value is set, the
750
         *      current number of the entry is returned. Elsewise, the value is set unchecked.
751
         *
752
         * Parameters:
753
         *      {Number} newValue - [optional] if set the new value of the number entry.
754
         *
755
         * Returns:
756
         *      This {NumberEntry} for chaining.
757
         */
758
        _value: function(newValue) {
759
            if (typeof newValue === 'undefined') {
760
                var val = this.inputs.val();
761
                if (!NUMBER_REGEX.test(val)) return window.NaN;
762
                return window.parseFloat(val);
763
            }
764
            this.inputs.val(newValue);
765
766
            return this;
767
        }
768
    });
769
770
    /**
771
     * Class: RangeEntry
772
     *      Entry for modifying values of a <Property::Range> consisting of two numbers inputs that bound the number
773
     *      range. This class is a concrete implementation of an <Entry>.
774
     */
775
    var RangeEntry = Entry.extend({
776
        /**
777
         * Method: blurEvents
778
         *      Overrides the default blur events. Since range entries consist of two number entries we need the exact
779
         *      same blur events here - meaning change is added due to HTML 5.
780
         *
781
         * Returns:
782
         *      {Array[String]} blurEvents - the blur events.
783
         */
784
        blurEvents: function() {
785
            return ['blur', 'change', 'remove'];
786
        },
787
788
        /**
789
         * Method: changeEvents
790
         *      Overrides the default change events. Same goes here as in blur events. We need the number input events
791
         *      here additionally. Therefore, keyup, cut and paste are added.
792
         *
793
         * Returns:
794
         *      {Array[String]} change Events - the change events.
795
         */
796
        changeEvents: function() {
797
            return ['keyup', 'cut', 'paste'];
798
        },
799
800
        /**
801
         * Method: fix
802
         *      Override of the empty default fix implementation. The behaviour here implements a usability convenience
803
         *      feature. Given that both inputs contain numbers, the value of the not modified value is always adjusted
804
         *      in a way that the two number frame a legal range, with the left value being the lower and right value to
805
         *      be the upper bound. Example: The left value contains the number 12 and the right as well. Now, the left
806
         *      value is increased by one. The value range is not legal anymore, being [13, 12]. So the right value is
807
         *      automatically adjusted to [13, 13].
808
         *
809
         * Parameters:
810
         *      {Event}      event - jQuery event object (see their documentation for specifics)
811
         *      {DOMElement} ui    - jQuery DOM element set that refer to the event handling element.
812
         *
813
         * Returns:
814
         *      This {RangeEntry} for chaining.
815
         */
816
        fix: function(event, ui) {
817
            var val = this._value();
818
            var lower = val[0];
819
            var upper = val[1];
820
821
            if (_.isNaN(lower) || _.isNaN(upper)) return this;
822
823
            var inputs = this.inputs.filter('input');
824
825
            var target = jQuery(event.target);
826
            if (target.is(inputs.eq(0)) && lower > upper) {
827
                this._value([lower, lower]);
828
            } else if (target.is(inputs.eq(1)) && upper < lower) {
829
                this._value([upper, upper]);
830
            }
831
832
            return this;
833
        },
834
835
        /**
836
         * Method: _setupVisualRepresentation
837
         *      Overrides the standard setup for visual representation container. The two number fields need a wrapping
838
         *      inline container.
839
         *
840
         * Returns:
841
         *      This {RangeEntry} for chaining.
842
         */
843
        _setupVisualRepresentation: function() {
844
            this._setupContainer()
845
                ._setupInput();
846
847
            jQuery('<form class="form-inline">')
848
                .append(this.inputs)
849
                .appendTo(this.container.find('.inputs'));
850
851
            this.setReadonly(this.property.readonly);
852
            this.setHidden(this.property.hidden);
853
854
            return this;
855
        },
856
857
        /**
858
         * Method:_setupInput
859
         *      Creates two numeric input fields as inline form fields as input. Also renders statically a small hyphen
860
         *      between them.
861
         *
862
         * Returns:
863
         *      This {RangeEntry} for chaining.
864
         */
865
        _setupInput: function() {
866
            var value = this.property.value;
867
            var min   = this.property.min;
868
            var max   = this.property.max;
869
            var step  = this.property.step;
870
871
            this.inputs = this._setupMiniNumeric(min, max, step, value[0]).css('width', '45%')
872
                .attr('id', this.id) // clicking the label should focus the first input
873
                .add(jQuery('<label> – </label>').css('width', '10%').css('text-align', 'center'))
874
                .add(this._setupMiniNumeric(min, max, step, value[1]).css('width', '45%'));
875
876
            return this;
877
        },
878
879
        /**
880
         * Method: _setupMiniNumeric
881
         *      Constructs and returns a number field with the given attributes.
882
         *
883
         * Parameters:
884
         *      {Number} min   - The minimum number that should be allowed.
885
         *      {Number} max   - The maximum number that should be allowed.
886
         *      {Number} step  - The step width the value should fit in.
887
         *      {Number} value - The currently set value.
888
         *
889
         *  Returns:
890
         *      The newly constructed mini number input {DOMElement}.
891
         */
892
        _setupMiniNumeric: function(min, max, step, value) {
893
            return jQuery('<input type="number" class="form-control input-small">')
894
                .attr('min',  this.property.min)
895
                .attr('max',  this.property.max)
896
                .attr('step', this.property.step)
897
                .val(value);
898
        },
899
900
        /**
901
         * Method: _value
902
         *      Concrete implementation of the _value getter/setter. If now new value is passed as parameter, the method
903
         *      functions as a getter and returns the current value as two-tuple/array of numbers. However, if a value
904
         *      is given, the method works as a setter. The value is set in the numeric input in order of the tuple.
905
         *
906
         * Parameters:
907
         *      {Array[Number]} newValue - [optional] the value to be set, if present.
908
         *
909
         * Returns:
910
         *      This {RangeEntry} for chaining.
911
         */
912
        _value: function(newValue) {
913
            var input = this.inputs.filter('input');
914
            var lower = input.eq(0);
915
            var upper = input.eq(1);
916
917
            if (typeof newValue === 'undefined') {
918
                var lowerVal = (!NUMBER_REGEX.test(lower.val()))
919
                    ? window.NaN : window.parseFloat(lower.val());
920
                var upperVal = (!NUMBER_REGEX.test(upper.val()))
921
                    ? window.NaN : window.parseFloat(upper.val());
922
                return [lowerVal, upperVal];
923
            }
924
            lower.val(newValue[0]);
925
            upper.val(newValue[1]);
926
927
            return this;
928
        }
929
    });
930
931
    /**
932
     * Class: EpsilonEntry
933
     *      Is a subclass of <RangeEntry> and also models an interval. However, the semantic is changed. The second
934
     *      number specifies an epsilon range around the first number and thereby creating the interval.
935
     */
936
    var EpsilonEntry = RangeEntry.extend({
937
        /**
938
         * Method: fix
939
         *      Overrides <RangeEntries> fix' method. The behaviour is altered in a manner that the epsilon range may
940
         *      never leave the possible minimum and maximum of the whole interval. Example: min/max of the whole
941
         *      interval is 0/1. You epsilon center is 0.75 and your epsilon value is 0.25. You now increase the center
942
         *      to 0.85, leaving the epsilon range to [0.6, 1.1]. The upper value exceeds the boundaries of the whole
943
         *      interval. So, the epsilon value is reduced down to 0.15, leaving its range as [0.7, 1].
944
         *
945
         * Parameters:
946
         *   {Event}      event - jQuery event object, refer to their documentation for specifics
947
         *   {DOMElement} ui    - The dom element that had the event handler registered.
948
         *
949
         * Returns:
950
         *      This {EpsilonEntry} for chaining.
951
         */
952
        fix: function(event, ui) {
953
            var val     = this._value();
954
            var center  = val[0];
955
            var epsilon = val[1];
956
957
            // early out, if one of the numbers is NaN we cannot fix anything, leave it to the property
958
            if (_.isNaN(center) || _.isNaN(epsilon)) return this;
959
960
            var pMin   = this.property.min;
961
            var pMax   = this.property.max;
962
            var target = jQuery(event.target);
963
964
            // early out, nothing to fix here
965
            if (pMin.gt(center) || pMax.lt(center) || pMax.lt(epsilon) || epsilon < 0) return this;
966
967
            var epsBounded = Math.min(Math.abs(pMin.toFloat() - center), epsilon, pMax.toFloat() - center);
968
            var cenBounded = Math.max(pMin.plus(epsilon), Math.min(center, pMax.minus(epsilon).toFloat()));
969
970
            if (epsilon == epsBounded && cenBounded == center) return this;
971
972
            if (target.is(this.inputs.eq(0))) {
973
                this._value([center, epsBounded]);
974
            } else if (target.is(this.inputs.eq(1))) {
975
                this._value([cenBounded, epsilon]);
976
            }
977
978
            return this;
979
        },
980
981
        /**
982
         * Method: _setupInput
983
         *      Overrides the parents behaviour by exchanging the label between the two input fields with a '±' sign.
984
         *
985
         * Returns:
986
         *      This {EpsilonEntry} for chaining.
987
         */
988
        _setupInput: function() {
989
            var value = this.property.value;
990
            var min   = this.property.min;
991
            var max   = this.property.max;
992
993
            this.inputs = this._setupMiniNumeric(min, max, this.property.step, value[0]).css('width', '45%')
994
                .attr('id', this.id) // clicking the label should focus the first input
995
                .add(jQuery('<label> ± </label>').css('width', '10%').css('text-align', 'center'))
996
                .add(this._setupMiniNumeric(0, max, this.property.epsilonStep, value[1]).css('width', '45%'));
997
998
            return this;
999
        }
1000
    });
1001
1002
    /**
1003
     * Class: TextEntry
1004
     *      Simple input field for a <Property::Text> and concrete implementation of <Entry>.
1005
     */
1006
    var TextEntry = Entry.extend({
1007
        /**
1008
         * Method: changeEvents
1009
         *      Overrides the default change events. Needs keyup, cut and paste events additionally.
1010
         *
1011
         * Returns:
1012
         *      {Array[String]} changeEvents - The change events.
1013
         */
1014
        changeEvents: function() {
1015
            return ['keyup', 'cut', 'paste'];
1016
        },
1017
1018
        /**
1019
         * Method: _setupInput
1020
         *      Creates the input element for a text entry - a text input box.
1021
         *
1022
         * Returns:
1023
         *      This {TextEntry} for chaining.
1024
         */
1025
        _setupInput: function() {
1026
            this.inputs = jQuery('<input type="text" class="form-control input-small">').attr('id', this.id);
1027
            return this;
1028
        },
1029
1030
        /**
1031
         * Method: _value
1032
         *      Getter/setter for the menu entry. If the optional parameter is not given, it works as a getter,
1033
         *      returning the inputs value as string. Otherwise, sets the passed value unchecked.
1034
         *
1035
         * Parameters:
1036
         *      {String} newValue - [optional] If present, the new value of the entry.
1037
         *
1038
         * Returns:
1039
         *   This {TextEntry} for chaining reasons, if used as setter.
1040
         *   The value as {String} otherwise.
1041
         */
1042
        _value: function(newValue) {
1043
            if (typeof newValue === 'undefined') return this.inputs.val();
1044
            this.inputs.val(newValue);
1045
1046
            return this;
1047
        }
1048
    });
1049
	
1050
	/**
1051
	 * Class: InlineTextArea
1052
     *      Special kind of text area property, that does NOT appear inside the properties TextArea for editing inside a
1053
     *      shape on the canvas. So far only used for editing inside a sticky note.
1054
	 */
1055
    var InlineTextArea = Entry.extend({
1056
        changeEvents: function() {
1057
            return ['keyup', 'cut', 'paste'];
1058
        },
1059
		
1060
        blurEvents: function() {
1061
            return ['blur'];
1062
        },
1063
1064
        /**
1065
         * Method: _setupInput
1066
         *      Implements the input setup by producing an HTML textarea. It is initially hidden.
1067
         *
1068
         * Returns:
1069
         *      This <InlineTextArea> for chaining.
1070
         */
1071
        _setupInput: function() {
1072
            this.inputs = jQuery('<textarea type="text" class="form-control">').attr('id', this.id);
1073
			//hide textarea at the beginning
1074
			this.inputs.toggle(false);
1075
1076
            return this;
1077
        },
1078
		
1079
        appendTo: function() {
1080
			this._setupCallbacks();
1081
            return this;
1082
        },
1083
1084
        /**
1085
         * Method
1086
         * @param event
1087
         * @param ui
1088
         */
1089
        blurred: function(event, ui) {
1090
			 this._super(event, ui);
1091
			 // hide textarea
1092
			 this.inputs.toggle(false);
1093
			 // show paragraph and set value
1094
			 this.inputs.siblings('p').html(
1095
				 escape(this.inputs.val())
1096
			 ).toggle(true);
1097
		},
1098
		
1099
        remove: function() {},
1100
		
1101
        _setupContainer: function() {
1102
			this.property.owner._nodeImage.append(
1103
				jQuery('<p align="center">').html(escape(this.property.value))
1104
			);
1105
			this.container = this.property.owner.container;
1106
			
1107
			return this;
1108
        },
1109
		
1110
        _setupVisualRepresentation: function() {
1111
            this._setupInput();
1112
			this._setupContainer();
1113
			this.container.find('.' + Factory.getModule('Config').Classes.EDITABLE).append(this.inputs);
1114
1115
            return this;
1116
        },
1117
1118
        _value: function(newValue) {
1119
            if (typeof newValue === 'undefined') return this.inputs.val();
1120
            this.inputs.val(newValue);
1121
1122
            return this;
1123
        }
1124
    });
1125
	
1126
    /**
1127
     * Class: TransferEntry
1128
     *      Allows to link to other entities in the database. Looks like a normal <ChoiceEntry>, but actually fetches
1129
     *      the available values from the backend using Ajax.
1130
     */
1131
    var TransferEntry = Entry.extend({
1132
        _progressIndicator: undefined,
1133
        _openButton: undefined,
1134
        _unlinked: undefined,
1135
1136
        init: function(property) {
1137
            this._super(property);
1138
1139
            jQuery(window).on('focus', this._refetchEntries.bind(this));
1140
            jQuery(this.property).on(Factory.getModule('Config').Events.PROPERTY_SYNCHRONIZED, this._refreshEntries.bind(this));
1141
        },
1142
1143
        blurEvents: function() {
1144
            return ['blur', 'change', 'remove'];
1145
        },
1146
1147
        fix: function(event, ui) {
1148
            if (this._value() !== this.property.UNLINK_VALUE) {
1149
                this._unlinked.remove();
1150
            }
1151
1152
            return this;
1153
        },
1154
1155
        setReadonly: function(readonly) {
1156
            this.inputs.attr('disabled', readonly ? 'disabled' : null);
1157
1158
            return this._super(readonly);
1159
        },
1160
1161
        _setupInput: function() {
1162
            this.inputs = jQuery('<select class="form-control input-small">')
1163
                .attr('id', this.id)
1164
                .css('display', 'none');
1165
1166
            // add placeholder entry
1167
            this._unlinked = jQuery('<option>')
1168
                .text(this.property.UNLINK_TEXT)
1169
                .attr('selected', 'selected')
1170
                .attr('value', this.property.UNLINK_VALUE);
1171
1172
            this._openButton = jQuery('<button type="button">')
1173
                .addClass('btn btn-default btn-small col-12')
1174
                .addClass(Factory.getModule('Config').Classes.PROPERTY_OPEN_BUTTON)
1175
                .text('Open in new tab')
1176
                .appendTo(this.container.children('.inputs'))
1177
                .css('display', 'none');
1178
1179
            return this._setupProgressIndicator();
1180
        },
1181
1182
        /**
1183
         *  Method: _setupOptions
1184
         *      Reconstructs the HTML option elements from the list of transfer graphs. It will also select
1185
         *      the currently active value or reset to default if the current value is no longer available.
1186
         *
1187
         *  Returns:
1188
         *      This {<TransferEntry>} instance for chaining.
1189
         */
1190
        _setupOptions: function() {
1191
            // remove old values
1192
            this.inputs.empty();
1193
1194
            var found = false;
1195
1196
            _.each(this.property.transferGraphs, function(graphName, graphID) {
1197
                var optionSelected = this.property.value == graphID;
1198
1199
                this.inputs.append(jQuery('<option>')
1200
                    .text(graphName)
1201
                    .attr('value', graphID)
1202
                    .attr('selected', optionSelected ? 'selected': null)
1203
                );
1204
                found |= optionSelected;
1205
            }.bind(this));
1206
1207
            // if the value was not found we need to reset to the default 'unlinked' value
1208
            if (!found) {
1209
                this.inputs.prepend(this._unlinked.attr('selected', 'selected'));
1210
                this.property.setValue(this.property.UNLINK_VALUE);
1211
            }
1212
1213
            return this;
1214
        },
1215
1216
        _setupCallbacks: function() {
1217
            this._openButton.click(function() {
1218
                var value = this._value();
1219
1220
                if (value != this.property.UNLINK_VALUE) {
1221
                    window.open(Factory.getModule('Config').Backend.EDITOR_URL + '/' + value, '_blank');
1222
                }
1223
            }.bind(this));
1224
1225
            return this._super();
1226
        },
1227
1228
        /**
1229
         *  Method: _setupProgressIndicator
1230
         *      Constructs the progress indicator that is displayed as long as the list is fetched with AJAX.
1231
         *
1232
         *  Returns:
1233
         *      This {<TransferEntry>} instance for chaining.
1234
         */
1235
        _setupProgressIndicator: function() {
1236
            this._progressIndicator = jQuery('<div class="progress progress-striped active">\
1237
                <div class="bar" style="width: 100%;"></div>\
1238
            </div>').appendTo(this.container.children('.inputs'));
1239
1240
            return this;
1241
        },
1242
1243
        /**
1244
         *  Method: _refetchEntries
1245
         *      Triggers a refetch of the available list values from the backend and displays the progress indicator.
1246
         *
1247
         *  Returns:
1248
         *      This {<TransferEntry>} instance for chaining.
1249
         */
1250
        _refetchEntries: function() {
1251
            this.property.fetchTransferGraphs();
1252
            this._progressIndicator.css('display', '');
1253
            this._openButton.css('display', 'none');
1254
            this.inputs.css('display', 'none');
1255
1256
            return this;
1257
        },
1258
1259
        /**
1260
         *  Method: _refreshEntries
1261
         *      Reconstructs the select list and hides the progress indicator.
1262
         *
1263
         *  Returns:
1264
         *      This {<TransferEntry>} instance for chaining.
1265
         */
1266
        _refreshEntries: function() {
1267
            this._setupOptions();
1268
            this._progressIndicator.css('display', 'none');
1269
            this._openButton.css('display', '');
1270
            this.inputs.css('display', '');
1271
1272
            return this;
1273
        },
1274
1275
        _value: function(newValue) {
1276
            if (typeof newValue === 'undefined') {
1277
                return window.parseInt(this.inputs.val());
1278
            }
1279
            this.inputs.val(newValue);
1280
1281
            return this;
1282
        }
1283
    });
1284
1285
    return {
1286
        'BoolEntry':     	BoolEntry,
1287
        'ChoiceEntry':   	ChoiceEntry,
1288
        'CompoundEntry': 	CompoundEntry,
1289
        'EpsilonEntry':  	EpsilonEntry,
1290
        'NumericEntry':  	NumericEntry,
1291
        'RangeEntry':    	RangeEntry,
1292
        'TextEntry':     	TextEntry,
1293
		'InlineTextArea':   InlineTextArea, 
1294
        'TransferEntry': 	TransferEntry,
1295
    }
1296
});
1297