src/Oro/Bundle/LocaleBundle/Resources/public/js/app/views/fallback-view.js   F
last analyzed

Complexity

Total Complexity 73
Complexity/F 1.83

Size

Lines of Code 530
Function Count 40

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
wmc 73
nc 128
mnd 2
bc 76
fnc 40
dl 0
loc 530
rs 3.9761
bpm 1.9
cpm 1.825
noi 1
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like src/Oro/Bundle/LocaleBundle/Resources/public/js/app/views/fallback-view.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(function(require) {
2
    'use strict';
3
4
    var FallbackView;
5
    var $ = require('jquery');
6
    var _ = require('underscore');
7
    var BaseView = require('oroui/js/app/views/base/view');
8
    var tinyMCE = require('tinymce/tinymce');
9
10
    /**
11
     * @export orolocale/js/app/views/fallback-view
12
     * @extends oroui.app.views.base.View
13
     * @class orolocale.app.views.FallbackView
14
     */
15
    FallbackView = BaseView.extend({
16
        autoRender: true,
17
18
        initSubviews: true,
19
20
        /**
21
         * @property {Object}
22
         */
23
        itemsByCode: {},
24
25
        /**
26
         * @property {Object}
27
         */
28
        itemToChildren: {},
29
30
        /**
31
         * @property {Object}
32
         */
33
        options: {
34
            expanded: false,
35
            hideDefaultLabel: true,
36
            fallbackWidth: 180,
37
            selectors: {
38
                status: '.fallback-status',
39
                item: '.fallback-item',
40
                defaultItem: '.fallback-item:first',
41
                childItem: '.fallback-item:not(:first)',
42
                itemLabel: '.fallback-item-label',
43
                itemValue: '.fallback-item-value',
44
                itemUseFallback: '.fallback-item-use-fallback',
45
                itemFallback: '.fallback-item-fallback'
46
            },
47
            icons: {
48
                'new': {
49
                    html: '<i class="fa-folder-o"></i>',
50
                    event: 'expandChildItems'
51
                },
52
                'edited': {
53
                    html: '<i class="fa-folder"></i>',
54
                    event: 'expandChildItems'
55
                },
56
                'save': {
57
                    html: '<i class="fa-folder-open"></i>',
58
                    event: 'collapseChildItems'
59
                }
60
            }
61
        },
62
63
        /**
64
         * @inheritDoc
65
         */
66
        constructor: function FallbackView() {
67
            FallbackView.__super__.constructor.apply(this, arguments);
68
        },
69
70
        /**
71
         * @inheritDoc
72
         */
73
        initialize: function(options) {
74
            this.options = _.defaults(options || {}, this.options);
75
            FallbackView.__super__.initialize.call(this, options);
76
        },
77
78
        /**
79
         * @inheritDoc
80
         */
81
        render: function() {
82
            this.$(this.options.selectors.childItem).attr('data-layout', 'separate');
83
84
            this._deferredRender();
85
            this.initLayout().done(function() {
86
                this.handleLayoutInit();
87
                this._resolveDeferredRender();
88
            }.bind(this));
89
90
            return this;
91
        },
92
93
        renderSubviews: function() {
94
            this.initSubviews = false;
95
            this.$(this.options.selectors.childItem).removeAttr('data-layout');
96
97
            this.initLayout().done(function() {
98
                this.bindEvents();
99
            }.bind(this));
100
        },
101
102
        /**
103
         * Doing something after loading child components
104
         */
105
        handleLayoutInit: function() {
106
            var self = this;
107
108
            this.mapItemsByCode();
109
110
            this.getUseFallbackEl(this.$el).each(function() {
111
                self.switchUseFallback(self.getItemEl(this));
112
            });
113
114
            this.mapItemToChildren();
115
116
            this.getValueEl(this.$el).each(function() {
117
                // self.cloneValueToChildren(self.getItemEl(this)); uncomment on merging master
118
            });
119
120
            this.fixFallbackWidth();
121
            this.setStatusIcon();
122
        },
123
124
        /**
125
         * Bind events to controls
126
         */
127
        bindEvents: function() {
128
            var self = this;
129
130
            this.getValueEl(this.$el)
131
                .change(_.bind(this.cloneValueToChildrenEvent, this))
132
                .keyup(_.bind(this.cloneValueToChildrenEvent, this));
133
134
            this.$el.find(this.options.selectors.itemValue).find('.mce-tinymce').each(function() {
135
                tinyMCE.get(self.getValueEl(self.getItemEl(this)).attr('id'))
136
                    .on('change', function() {
137
                        $(this.targetElm).change();
138
                    })
139
                    .on('keyup', function() {
140
                        $(this.targetElm).change();
141
                    });
142
            });
143
144
            this.getUseFallbackEl(this.$el)
145
                .change(_.bind(this.switchUseFallbackEvent, this));
146
147
            this.getFallbackEl(this.$el)
148
                .change(_.bind(this.switchFallbackTypeEvent, this));
149
        },
150
151
        /**
152
         * Create item code to element mapping
153
         */
154
        mapItemsByCode: function() {
155
            var self = this;
156
157
            this.itemsByCode = {};
158
159
            this.$el.find(this.options.selectors.item).each(function() {
160
                var $item = $(this);
161
                var itemCode = self.getItemCode($item);
162
163
                if (!itemCode) {
164
                    return;
165
                }
166
167
                self.itemsByCode[itemCode] = $item;
168
            });
169
        },
170
171
        /**
172
         * Create item to children mapping
173
         */
174
        mapItemToChildren: function() {
175
            var self = this;
176
177
            this.itemToChildren = {};
178
179
            this.$el.find(this.options.selectors.item).each(function() {
180
                var $item = $(this);
181
                var parentItemCode = self.getParentItemCode($item);
182
183
                if (!parentItemCode) {
184
                    return;
185
                }
186
187
                if (self.itemToChildren[parentItemCode] === undefined) {
188
                    self.itemToChildren[parentItemCode] = [];
189
                }
190
                self.itemToChildren[parentItemCode].push($item);
191
            });
192
        },
193
194
        /**
195
         * Trigger on value change
196
         *
197
         * @param {Event} e
198
         */
199
        cloneValueToChildrenEvent: function(e) {
200
            this.cloneValueToChildren(this.getItemEl(e.currentTarget));
201
        },
202
203
        /**
204
         * Trigger on "use fallback" change
205
         *
206
         * @param {Event} e
207
         */
208
        switchUseFallbackEvent: function(e) {
209
            this.switchUseFallback(this.getItemEl(e.currentTarget));
210
        },
211
212
        /**
213
         * Trigger on fallback change
214
         *
215
         * @param {Event} e
216
         */
217
        switchFallbackTypeEvent: function(e) {
218
            var $item = this.getItemEl(e.currentTarget);
219
220
            this.mapItemToChildren();
221
222
            var parentItemCode = this.getParentItemCode($item);
223
            if (parentItemCode) {
224
                var $fromValue = this.getValueEl(this.itemsByCode[parentItemCode]);
225
                var $toValue = this.getValueEl($item);
226
                this.cloneValue($fromValue, $toValue);
227
            } else {
228
                this.cloneValueToChildrenEvent(e);
229
            }
230
        },
231
232
        /**
233
         * Show child items
234
         */
235
        expandChildItems: function() {
236
            if (this.initSubviews) {
237
                this.renderSubviews();
238
            }
239
240
            this.options.expanded = true;
241
            this.setStatusIcon();
242
        },
243
244
        /**
245
         * Hide child items
246
         */
247
        collapseChildItems: function() {
248
            this.options.expanded = false;
249
            this.setStatusIcon();
250
        },
251
252
        /**
253
         * Clone item value to children
254
         *
255
         * @param {jQuery} $item
256
         */
257
        cloneValueToChildren: function($item) {
258
            var $fromValue = this.getValueEl($item);
259
            var itemCode = this.getItemCode($item);
260
261
            var self = this;
262
            $.each(this.itemToChildren[itemCode] || [], function() {
263
                var $toValue = self.getValueEl(this);
264
                self.cloneValue($fromValue, $toValue);
265
            });
266
        },
267
268
        /**
269
         * Enable/disable controls depending on the "use fallback"
270
         *
271
         * @param {jQuery} $item
272
         */
273
        switchUseFallback: function($item) {
274
            var $useFallback = this.getUseFallbackEl($item);
275
            if ($useFallback.length === 0) {
276
                return;
277
            }
278
279
            var checked = $useFallback.get(0).checked;
280
281
            this.enableDisableValue(this.getValueEl($item), !checked);
282
            this.enableDisableFallback(this.getFallbackEl($item), checked);
283
        },
284
285
        /**
286
         * Enable/disable value
287
         *
288
         * @param {jQuery} $value
289
         * @param {Boolean} enable
290
         */
291
        enableDisableValue: function($value, enable) {
292
            var $valueContainer = $value.closest(this.options.selectors.itemValue);
293
294
            var editor;
295
            if ($valueContainer.find('.mce-tinymce').length > 0) {
296
                editor = tinyMCE.get($valueContainer.find('textarea').attr('id'));
297
            }
298
299
            if (enable) {
300
                $value.removeAttr('disabled');
301
302
                if (editor) {
303
                    editor.getBody().setAttribute('contenteditable', true);
304
                    $(editor.editorContainer).removeClass('disabled');
305
                    $(editor.editorContainer).children('.disabled-overlay').remove();
306
                }
307
            } else {
308
                $value.attr('disabled', 'disabled');
309
310
                if (editor) {
311
                    editor.getBody().setAttribute('contenteditable', false);
312
                    $(editor.editorContainer).addClass('disabled');
313
                    $(editor.editorContainer).append('<div class="disabled-overlay"></div>');
314
                }
315
            }
316
        },
317
318
        /**
319
         * Enable/disable fallback
320
         *
321
         * @param {jQuery} $fallback
322
         * @param {Boolean} enable
323
         */
324
        enableDisableFallback: function($fallback, enable) {
325
            var $fallbackContainer = $fallback.inputWidget('container');
326
327
            if (enable) {
328
                $fallback.removeAttr('disabled');
329
330
                if ($fallbackContainer) {
331
                    $fallbackContainer.removeClass('disabled');
332
                }
333
            } else {
334
                $fallback.attr('disabled', 'disabled');
335
336
                if ($fallbackContainer) {
337
                    $fallbackContainer.addClass('disabled');
338
                }
339
            }
340
341
            $fallback.change();
342
        },
343
344
        /**
345
         * Clone value to another value
346
         *
347
         * @param {jQuery} $fromValue
348
         * @param {jQuery} $toValue
349
         */
350
        cloneValue: function($fromValue, $toValue) {
351
            var isChanged = false;
352
            $fromValue.each(function(i) {
353
                var toValue = $toValue.get(i);
354
                if ($(this).is(':checkbox') || $(this).is(':radio')) {
355
                    if (toValue.checked !== this.checked) {
356
                        isChanged = true;
357
                        toValue.checked = this.checked;
358
                    }
359
                } else {
360
                    if ($(toValue).val() !== $(this).val()) {
361
                        isChanged = true;
362
                        $(toValue).val($(this).val());
363
                    }
364
                }
365
            });
366
            if (isChanged) {
367
                $toValue.filter(':first').change();
368
            }
369
        },
370
371
        /**
372
         * Get item element by children
373
         *
374
         * @param {*|jQuery|HTMLElement} el
375
         *
376
         * @returns {jQuery}
377
         */
378
        getItemEl: function(el) {
379
            var $item = $(el);
380
            if (!$item.is(this.options.selectors.item)) {
381
                $item = $item.closest(this.options.selectors.item);
382
            }
383
            return $item;
384
        },
385
386
        /**
387
         * Get value element
388
         *
389
         * @param {jQuery} $el
390
         *
391
         * @returns {jQuery}
392
         */
393
        getValueEl: function($el) {
394
            return $el.find(this.options.selectors.itemValue).find('input, textarea, select');
395
        },
396
397
        /**
398
         * Get "use fallback" element
399
         *
400
         * @param {jQuery} $el
401
         *
402
         * @returns {jQuery}
403
         */
404
        getUseFallbackEl: function($el) {
405
            return $el.find(this.options.selectors.itemUseFallback).find('input');
406
        },
407
408
        /**
409
         * Get fallback element
410
         *
411
         * @param {jQuery} $el
412
         *
413
         * @returns {jQuery}
414
         */
415
        getFallbackEl: function($el) {
416
            return $el.find(this.options.selectors.itemFallback).find('select');
417
        },
418
419
        /**
420
         * Get parent item code
421
         *
422
         * @param {jQuery} $item
423
         *
424
         * @returns {undefined|String}
425
         */
426
        getParentItemCode: function($item) {
427
            var $select = this.getFallbackEl($item);
428
            if ($select.length === 0 || $select.attr('disabled')) {
429
                return;
430
            }
431
432
            var parentItemCode = $select.attr('data-parent-localization');
433
434
            return parentItemCode && $select.val() !== 'system' ? parentItemCode : $select.val();
435
        },
436
437
        /**
438
         * Get item code
439
         *
440
         * @param {jQuery} $item
441
         *
442
         * @returns {String}
443
         */
444
        getItemCode: function($item) {
445
            var $select = this.getFallbackEl($item);
446
            var itemCode;
447
448
            if ($select.length === 0) {
449
                itemCode = 'system';
450
            } else {
451
                itemCode = $select.attr('data-localization');
452
            }
453
454
            return itemCode;
455
        },
456
457
        /**
458
         * Check is child has custom value
459
         */
460
        isChildEdited: function() {
461
            var isChildEdited = false;
462
            var $childItems = this.$el.find(this.options.selectors.childItem);
463
464
            this.getValueEl($childItems).each(function() {
465
                if (this.disabled) {
466
                    return;
467
                }
468
469
                if ($(this).is(':checkbox') || $(this).is(':radio')) {
470
                    isChildEdited = true;
471
                } else {
472
                    isChildEdited = $(this).val().length > 0;
473
                }
474
475
                if (isChildEdited) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if isChildEdited is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
476
                    return false;
477
                }
478
            });
479
480
            return isChildEdited;
481
        },
482
483
        /**
484
         * Set fallback selector width depending of their content
485
         */
486
        fixFallbackWidth: function() {
487
            var $fallback = this.$el.find(this.options.selectors.itemFallback).find('select');
488
            $fallback.inputWidget('width', this.options.fallbackWidth);
489
        },
490
491
        /**
492
         * Change status icon depending on expanded flag and child custom values
493
         */
494
        setStatusIcon: function() {
495
            var icon;
496
497
            if (this.options.expanded) {
498
                icon = 'save';
499
            } else if (this.isChildEdited()) {
500
                icon = 'edited';
501
            } else {
502
                icon = 'new';
503
            }
504
505
            icon = this.options.icons[icon];
506
507
            this.$el.find(this.options.selectors.status)
508
                .html(icon.html)
509
                .find('i').click(_.bind(this[icon.event], this));
510
511
            var $defaultLabel = this.$el.find(this.options.selectors.defaultItem)
512
                .find(this.options.selectors.itemLabel);
513
            var $childItems = this.$el.find(this.options.selectors.childItem);
514
515
            if (this.options.expanded) {
516
                if (this.options.hideDefaultLabel) {
517
                    $defaultLabel.show();
518
                }
519
                $childItems.show();
520
            } else {
521
                if (this.options.hideDefaultLabel) {
522
                    $defaultLabel.hide();
523
                }
524
                $childItems.hide();
525
            }
526
        }
527
    });
528
529
    return FallbackView;
530
});
531