F
last analyzed

Complexity

Conditions 20
Paths 556

Size

Total Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
nc 556
dl 0
loc 67
rs 3.3134
c 0
b 0
f 0
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like scroll-helper.js ➔ ... ➔ scrollHelper.getVisibleRect 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 $ = require('jquery');
5
    var _ = require('underscore');
6
    var tools = require('oroui/js/tools');
7
    require('jquery-ui');
8
9
    var scrollHelper = {
10
        /**
11
         * Height of header on mobile devices
12
         */
13
        MOBILE_HEADER_HEIGHT: 54,
14
15
        /**
16
         * Height of header on mobile devices
17
         */
18
        MOBILE_POPUP_HEADER_HEIGHT: 44,
19
20
        /**
21
         * Cached scrollbarWidth value
22
         */
23
        _scrollbarWidth: -1,
24
25
        /**
26
         * Select global scrollable container
27
         */
28
        _scrollableContainerSelector: '#container',
29
30
        /**
31
         * Store scroll position
32
         */
33
        _scrollState: null,
34
35
        /**
36
         * Disable/Enable scroll state
37
         */
38
        _isBodyTouchScrollDisabled: false,
39
40
        /**
41
         * Disable body scroll on touch devices
42
         * @returns {boolean}
43
         */
44
        disableBodyTouchScroll: function() {
45
            if (this._isBodyTouchScrollDisabled) {
46
                return false;
47
            }
48
49
            this._scrollState = window.scrollY;
50
51
            $(this._scrollableContainerSelector)
52
                .addClass('disable-touch-scrolling')
53
                .css('height', window.innerHeight);
54
55
            $(document)
56
                .off('touchmove.disableScroll')
57
                .on('touchmove.disableScroll', _.bind(this._preventMobileScrolling, this));
58
59
            this._isBodyTouchScrollDisabled = true;
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
60
        },
61
62
        /**
63
         * Enable body scroll on touch devices
64
         * @returns {boolean}
65
         */
66
        enableBodyTouchScroll: function() {
67
            if (!this._isBodyTouchScrollDisabled) {
68
                return false;
69
            }
70
71
            $(this._scrollableContainerSelector)
72
                .removeClass('disable-touch-scrolling')
73
                .css('height', '');
74
75
            $(document).off('touchmove.disableScroll');
76
77
            window.scrollTo(0, this._scrollState);
78
79
            this._isBodyTouchScrollDisabled = false;
80
            this._scrollState = null;
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
81
        },
82
83
        /**
84
         * Block touch move event propagation on body
85
         * @param {jQueryEvent} event
86
         * @private
87
         */
88
        _preventMobileScrolling: function(event) {
89
            var isTouchMoveAllowed = true;
90
            var target = event.target;
91
92
            while (target !== null) {
93
                if (target.classList && target.classList.contains('disable-scrolling')) {
94
                    isTouchMoveAllowed = false;
95
                    break;
96
                }
97
                target = target.parentNode;
98
            }
99
100
            if (!isTouchMoveAllowed) {
101
                event.preventDefault();
102
            }
103
        },
104
105
        /**
106
         * Remove bounce effect, when scroll overflow viewport
107
         * @param {jQueryEvent} event
108
         */
109
        removeIOSRubberEffect: function(event) {
110
            var element = event.currentTarget;
111
            var top = element.scrollTop;
112
            var totalScroll = element.scrollHeight;
113
            var currentScroll = top + element.offsetHeight;
114
115
            if (top === 0) {
116
                element.scrollTop = 1;
117
            } else if (currentScroll === totalScroll) {
118
                element.scrollTop = top - 1;
119
            }
120
        },
121
        /**
122
         * Try to calculate the scrollbar width for your browser/os
123
         * @return {Number}
124
         */
125
        scrollbarWidth: function() {
126
            if (this._scrollbarWidth === -1) {
127
                this._scrollbarWidth = $.position.scrollbarWidth();
128
            }
129
            return this._scrollbarWidth;
130
        },
131
132
        /**
133
         * Returns true if el has visible scroll
134
         */
135
        hasScroll: function(el, a) {
136
            if ($(el).css('overflow') === 'hidden') {
137
                return false;
138
            }
139
            var scroll = (a && a === 'left') ? 'scrollLeft' : 'scrollTop';
140
            if (el[scroll] > 0) {
141
                return true;
142
            }
143
            el[scroll] = 1;
144
            var has = (el[scroll] > 0);
145
            el[scroll] = 0;
146
            return has;
147
        },
148
149
        /**
150
         * Cached documentHeight value
151
         */
152
        _documentHeight: -1,
153
154
        /**
155
         * Returns actual documentHeight
156
         * @return {Number}
157
         */
158
        documentHeight: function() {
159
            if (this._documentHeight === -1) {
160
                this._documentHeight = $(document).height();
161
            }
162
            return this._documentHeight;
163
        },
164
165
        /**
166
         * Returns visible rect of DOM element
167
         *
168
         * @param el
169
         * @param {{top: number, left: number, bottom: number, right: number}} increments for each initial rect side
170
         * @param {boolean} forceInvisible if true - function will return initial rect when element is out of screen
171
         * @param {Function} onAfterGetClientRect - callback called after each getBoundingClientRect
172
         * @returns {{top: number, left: number, bottom: number, right: number}}
173
         */
174
        getVisibleRect: function(el, increments, forceInvisible, onAfterGetClientRect) {
175
            increments = increments || {};
176
            _.defaults(increments, {
177
                top: 0,
178
                left: 0,
179
                bottom: 0,
180
                right: 0
181
            });
182
            var current = el;
183
            var midRect = this.getEditableClientRect(current);
184
            if (onAfterGetClientRect) {
185
                onAfterGetClientRect(current, midRect);
186
            }
187
            var borders;
188
            var resultRect = {
189
                top: midRect.top + increments.top,
190
                left: midRect.left + increments.left,
191
                bottom: midRect.bottom + increments.bottom,
192
                right: midRect.right + increments.right
193
            };
194
            if (
195
                (resultRect.top === 0 && resultRect.bottom === 0) || // no-data block is shown
196
                (resultRect.top > this.documentHeight() && forceInvisible)
197
            ) {
198
                // no need to calculate anything
199
                return resultRect;
200
            }
201
            current = current.parentNode;
202
            while (current && current.getBoundingClientRect) {
203
                /**
204
                 * Equals header height. Cannot calculate dynamically due to issues on ipad
205
                 */
206
                if (resultRect.top < this.MOBILE_HEADER_HEIGHT && tools.isMobile()) {
207
                    if (current.id === 'top-page' && !$(document.body).hasClass('input-focused')) {
208
                        resultRect.top = this.MOBILE_HEADER_HEIGHT;
209
                    } else if (current.className.split(/\s+/).indexOf('widget-content') !== -1) {
210
                        resultRect.top = this.MOBILE_POPUP_HEADER_HEIGHT;
211
                    }
212
                }
213
214
                midRect = this.getFinalVisibleRect(current, onAfterGetClientRect);
215
                borders = $.fn.getBorders(current);
216
217
                var style = window.getComputedStyle(current);
218
                if (style.overflowX !== 'visible' || style.overflowY !== 'visible') {
219
                    if (resultRect.top < midRect.top + borders.top) {
220
                        resultRect.top = midRect.top + borders.top;
221
                    }
222
                    if (resultRect.bottom > midRect.bottom - borders.bottom) {
223
                        resultRect.bottom = midRect.bottom - borders.bottom;
224
                    }
225
                    if (resultRect.left < midRect.left + borders.left) {
226
                        resultRect.left = midRect.left + borders.left;
227
                    }
228
                    if (resultRect.right > midRect.right - borders.right) {
229
                        resultRect.right = midRect.right - borders.right;
230
                    }
231
                }
232
                current = current.parentNode;
233
            }
234
235
            if (resultRect.top < 0) {
236
                resultRect.top = 0;
237
            }
238
239
            return resultRect;
240
        },
241
242
        getFinalVisibleRect: function(current, onAfterGetClientRect) {
243
            var rect = this.getEditableClientRect(current);
244
            if (onAfterGetClientRect) {
245
                onAfterGetClientRect(current, rect);
246
            }
247
248
            var border = $.fn.getBorders(current);
249
            var verticalScrollIsVisible = (current.offsetWidth - border.left - border.right) > current.clientWidth;
250
            var horizontalScrollIsVisible = (current.offsetHeight - border.top - border.bottom) > current.clientHeight;
251
252
            if (horizontalScrollIsVisible && current.scrollHeight > current.clientHeight) {
253
                rect.bottom -= this.scrollbarWidth();
254
            }
255
            if (verticalScrollIsVisible && current.scrollWidth > current.clientWidth) {
256
                rect.right -= this.scrollbarWidth();
257
            }
258
            return rect;
259
        },
260
261
        getEditableClientRect: function(el) {
262
            var rect = el.getBoundingClientRect();
263
            return {
264
                top: rect.top,
265
                left: rect.left,
266
                bottom: rect.bottom,
267
                right: rect.right
268
            };
269
        },
270
271
        isCompletelyVisible: function(el, onAfterGetClientRect) {
272
            var rect = this.getEditableClientRect(el);
273
            if (onAfterGetClientRect) {
274
                onAfterGetClientRect(el, rect);
275
            }
276
            if (rect.top === rect.bottom || rect.left === rect.right) {
277
                return false;
278
            }
279
            var visibleRect = this.getVisibleRect(el, null, false, onAfterGetClientRect);
280
            return visibleRect.top === rect.top &&
281
                visibleRect.bottom === rect.bottom &&
282
                visibleRect.left === rect.left &&
283
                visibleRect.right === rect.right;
284
        },
285
286
        scrollIntoView: function(el, onAfterGetClientRect, verticalGap, horizontalGap) {
287
            if (this.isCompletelyVisible(el, onAfterGetClientRect)) {
288
                return {vertical: 0, horizontal: 0};
289
            }
290
291
            var rect = this.getEditableClientRect(el);
292
            if (onAfterGetClientRect) {
293
                onAfterGetClientRect(el, rect);
294
            }
295
            if (rect.top === rect.bottom || rect.left === rect.right) {
296
                return {vertical: 0, horizontal: 0};
297
            }
298
            var visibleRect = this.getVisibleRect(el, null, false, onAfterGetClientRect);
299
            var scrolls = {
300
                vertical: rect.top !== visibleRect.top ? visibleRect.top - rect.top
301
                    : (rect.bottom !== visibleRect.bottom ? visibleRect.bottom - rect.bottom : 0),
302
                horizontal: rect.left !== visibleRect.left ? visibleRect.left - rect.left
303
                    : (rect.right !== visibleRect.right ? visibleRect.right - rect.right : 0)
304
            };
305
306
            if (verticalGap && scrolls.vertical) {
307
                scrolls.vertical += verticalGap * Math.sign(scrolls.vertical);
308
            }
309
310
            if (horizontalGap && scrolls.horizontal) {
311
                scrolls.horizontal += horizontalGap * Math.sign(scrolls.horizontal);
312
            }
313
314
            return this.applyScrollToParents(el, scrolls);
315
        },
316
317
        applyScrollToParents: function(el, scrolls) {
318
            if (!scrolls.horizontal && !scrolls.vertical) {
319
                return scrolls;
320
            }
321
322
            // make a local copy to don't change initial object
323
            scrolls = _.extend({}, scrolls);
324
325
            $(el).parents().each(function() {
326
                var $this = $(this);
327
                if (scrolls.horizontal !== 0) {
328
                    switch ($this.css('overflowX')) {
329
                        case 'auto':
330
                        case 'scroll':
331
                            if (this.clientWidth < this.scrollWidth) {
332
                                var oldScrollLeft = this.scrollLeft;
333
                                this.scrollLeft = this.scrollLeft - scrolls.horizontal;
334
                                scrolls.horizontal += this.scrollLeft - oldScrollLeft;
335
                            }
336
                            break;
337
                        default:
338
                            break;
339
                    }
340
                }
341
                if (scrolls.vertical !== 0) {
342
                    switch ($this.css('overflowY')) {
343
                        case 'auto':
344
                        case 'scroll':
345
                            if (this.clientHeight < this.scrollHeight) {
346
                                var oldScrollTop = this.scrollTop;
347
                                this.scrollTop = this.scrollTop - scrolls.vertical;
348
                                scrolls.vertical += this.scrollTop - oldScrollTop;
349
                            }
350
                            break;
351
                        default:
352
                            break;
353
                    }
354
                }
355
            });
356
357
            return scrolls;
358
        }
359
    };
360
361
    // reset document height cache on resize
362
    $(window).bindFirst('resize', function() {
363
        scrollHelper._documentHeight = -1;
364
    });
365
366
    return scrollHelper;
367
});
368