always.js   D
last analyzed

Complexity

Total Complexity 71
Complexity/F 1.69

Size

Lines of Code 391
Function Count 42

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 0
wmc 71
c 1
b 0
f 0
nc 88
mnd 2
bc 69
fnc 42
dl 0
loc 391
rs 4.0769
bpm 1.6428
cpm 1.6904
noi 1

27 Functions

Rating   Name   Duplication   Size   Complexity  
A Always.detach 0 4 1
B ➔ Always 0 28 1
A ➔ AlwaysData 0 4 1
A Always.attach 0 7 2
A AlwaysData.constructor 0 9 1
A Always.data 0 9 2
A Always.normalizeSelector 0 5 1
B Always.never 0 21 6
A Always.notifyCallbacks 0 18 2
A Always.removeCallback 0 15 4
A Always.always 0 18 3
A Always.addCallback 0 8 2
B Always.constructor 0 290 1
A ➔ Polyfills 0 2 1
A Always.removeRemovedCallback 0 3 1
A Polyfills.constructor 0 7 1
A Always.addInsertedCallback 0 3 1
A Always.notifyRemoved 0 12 3
A Always.removeInsertedCallback 0 3 1
A ➔ bindingNative 0 6 1
A Always.addRemovedCallback 0 3 1
A ➔ polyfillElementMatches 0 22 2
A Always.notifyInserted 0 12 3
A ➔ polyfillMutationObserver 0 6 2
A ➔ Bindings 0 2 1
A ➔ bindingJQuery 0 14 1
A Bindings.constructor 0 7 1

How to fix   Complexity   

Complexity

Complex classes like always.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
(function (jQuery) {
2
'use strict';
3
4
jQuery = jQuery && jQuery.hasOwnProperty('default') ? jQuery['default'] : jQuery;
5
6
var AlwaysData = /** @class */ (function () {
7
    function AlwaysData() {
8
        this.instance = null;
9
        this.lastOperation = null;
10
    }
11
    AlwaysData.OPERATION_INSERTION = 0;
12
    AlwaysData.OPERATION_REMOVAL = 1;
13
    return AlwaysData;
14
}());
15
16
var Always = /** @class */ (function () {
17
    /**
18
     * Constructor.
19
     *
20
     * @param {HTMLElement} element
21
     */
22
    function Always(element) {
23
        var _this = this;
24
        this.insertedCallbacks = {};
25
        this.removedCallbacks = {};
26
        this.element = element;
27
        this.observer = new MutationObserver(function (mutations) { return mutations.forEach(function (mutation) {
28
            if ('childList' !== mutation.type) {
29
                return;
30
            }
31
            // NodeList does not support forEach directly due to a bug in Google Chrome
32
            [].forEach.call(mutation.addedNodes, function (node) {
33
                if (!(node instanceof HTMLElement)) {
34
                    return;
35
                }
36
                _this.notifyInserted(node);
37
            });
38
            [].forEach.call(mutation.removedNodes, function (node) {
39
                if (!(node instanceof HTMLElement)) {
40
                    return;
41
                }
42
                _this.notifyRemoved(node);
43
            });
44
        }); });
45
        this.observer.observe(this.element, {
46
            childList: true,
47
            subtree: true
48
        });
49
    }
50
    /**
51
     * Retrieve a jQuery Always specific data object assigned to the specified element, or create one if it does
52
     * not already exist.
53
     *
54
     * @param {HTMLElement} element
55
     * @returns {AlwaysData}
56
     */
57
    Always.data = function (element) {
58
        if (!element.hasOwnProperty('jQueryAlways')) {
59
            Object.defineProperty(element, 'jQueryAlways', {
60
                value: new AlwaysData(),
61
                configurable: true
62
            });
63
        }
64
        return element.jQueryAlways;
65
    };
66
    /**
67
     * Attaches a new Always instance to the specified element and returns it, or returns a previously attached
68
     * instance.
69
     *
70
     * @param {HTMLElement} element
71
     * @returns {Always}
72
     */
73
    Always.attach = function (element) {
74
        var data = this.data(element);
75
        if (!data.instance) {
76
            data.instance = new Always(element);
77
        }
78
        return data.instance;
79
    };
80
    /**
81
     * Detaches a previously attached Always instance from the specified element & also removes the mutation
82
     * observer.
83
     *
84
     * @param {HTMLElement} element
85
     */
86
    Always.detach = function (element) {
87
        Always.attach(element).observer.disconnect();
88
        delete element.jQueryAlways;
89
    };
90
    /**
91
     * Normalizes similar selectors.
92
     * E.g. "a, b" and "b,a" are the same thing, both will be normalized to "a,b".
93
     *
94
     * @param {string} selector
95
     * @returns {string}
96
     */
97
    Always.normalizeSelector = function (selector) {
98
        return selector.split(',').map(function (part) {
99
            return part.trim();
100
        }).sort().join(',');
101
    };
102
    /**
103
     * Attaches the specified inserted / removed listeners for elements matching the specified selector on the
104
     * specified parent element.
105
     *
106
     * @param {HTMLElement} element
107
     * @param {string} selector
108
     * @param {() => void} onInserted
109
     * @param {() => void} onRemoved
110
     */
111
    Always.always = function (element, selector, onInserted, onRemoved) {
112
        var instance = Always.attach(element);
113
        // register inserted callbacks
114
        if ('function' === typeof onInserted) {
115
            instance.addInsertedCallback(selector, onInserted);
116
            // invoke the inserted callback on all matching elements already in the dom
117
            // this invocation is always forced even if the element's last operation was an insertion
118
            // we still need to mark the element, though, to avoid future duplicate notifies
119
            [].forEach.call(element.querySelectorAll(selector), function (node) {
120
                Always.data(node).lastOperation = AlwaysData.OPERATION_INSERTION;
121
                onInserted.call(node);
122
            });
123
        }
124
        // register removed callbacks
125
        if ('function' === typeof onRemoved) {
126
            instance.addRemovedCallback(selector, onRemoved);
127
        }
128
    };
129
    /**
130
     * Detaches the specified inserted / removed listener(s) for elements matching the specified selector on the
131
     * specified element as parent.
132
     *
133
     * @param {HTMLElement} element
134
     * @param {string} selector
135
     * @param {() => void} onInserted
136
     * @param {() => void} onRemoved
137
     */
138
    Always.never = function (element, selector, onInserted, onRemoved) {
139
        // if no selector is specified, quickest way to remove all listeners is to just detach
140
        if (!selector) {
141
            Always.detach(element);
142
            return;
143
        }
144
        var instance = Always.attach(element);
145
        // if no specific callback is requested, remove all listeners that match the selector
146
        if (!onInserted && !onRemoved) {
147
            instance.removeInsertedCallback(selector);
148
            instance.removeRemovedCallback(selector);
149
            return;
150
        }
151
        // remove only specific listeners
152
        if (onInserted) {
153
            instance.removeInsertedCallback(selector, onInserted);
154
        }
155
        if (onRemoved) {
156
            instance.removeRemovedCallback(selector, onRemoved);
157
        }
158
    };
159
    /**
160
     * Adds a new callback for the specified selector.
161
     *
162
     * @param {{[p: string]: (() => void)[]}} callbacks
163
     * @param {string} selector
164
     * @param {() => void} callback
165
     * @returns {Always}
166
     */
167
    Always.prototype.addCallback = function (callbacks, selector, callback) {
168
        selector = Always.normalizeSelector(selector);
169
        if (!callbacks.hasOwnProperty(selector)) {
170
            callbacks[selector] = [];
171
        }
172
        callbacks[selector].push(callback);
173
        return this;
174
    };
175
    /**
176
     * Removes the specified callback for the specified selector.
177
     * If no callback is specified, removes all callbacks for the selector.
178
     *
179
     * @param {{[p: string]: (() => void)[]}} callbacks
180
     * @param {string} selector
181
     * @param {() => void} callback
182
     * @returns {Always}
183
     */
184
    Always.prototype.removeCallback = function (callbacks, selector, callback) {
185
        selector = Always.normalizeSelector(selector);
186
        if (!callbacks.hasOwnProperty(selector)) {
187
            return this;
188
        }
189
        if (callback) {
190
            for (var index = -1; -1 < (index = callbacks[selector].indexOf(callback));) {
0 ignored issues
show
Unused Code introduced by
The assignment to variable index seems to be never used. Consider removing it.
Loading history...
191
                callbacks[selector].splice(index, 1);
192
            }
193
        }
194
        else {
195
            delete callbacks[selector];
196
        }
197
        return this;
198
    };
199
    /**
200
     * Notifies the specified callbacks for the specified operation on the specified element.
201
     * This is a convenience method for notifyInserted and notifyRemoved.
202
     *
203
     * @param {HTMLElement} element
204
     * @param {{[p: string]: (() => void)[]}} callbacks
205
     * @param {number} operation
206
     * @returns {Always}
207
     */
208
    Always.prototype.notifyCallbacks = function (element, callbacks, operation) {
209
        // make the element manageable & prevent duplicate invocations
210
        // even making every single node in the Dom manageable is still much faster that matching it against a selector
211
        var data = Always.data(element);
212
        if (operation === data.lastOperation) {
213
            return this;
214
        }
215
        data.lastOperation = operation;
216
        // traverse requested callbacks & invoke those for which the element matches the corresponding selector
217
        Object.keys(callbacks).forEach(function (selector) {
218
            if (element.matches(selector)) {
219
                callbacks[selector].forEach(function (callback) {
220
                    callback.call(element);
221
                });
222
            }
223
        });
224
        return this;
225
    };
226
    /**
227
     * Notifies all registered callbacks about an insertion, if the corresponding selector matches the node.
228
     *
229
     * @param {HTMLElement} element
230
     * @returns {Always}
231
     */
232
    Always.prototype.notifyInserted = function (element) {
233
        // callbacks for insertions are invoked for parents first
234
        this.notifyCallbacks(element, this.insertedCallbacks, AlwaysData.OPERATION_INSERTION);
235
        // we need to manually cascade notify all child nodes as the observer won't do it automatically
236
        if (element.children) {
237
            for (var _i = 0, _a = element.children; _i < _a.length; _i++) {
238
                var node = _a[_i];
239
                this.notifyInserted(node);
240
            }
241
        }
242
        return this;
243
    };
244
    /**
245
     * Notifies all registered callbacks about a removal, if the corresponding selector matches the node.
246
     *
247
     * @param {HTMLElement} element
248
     * @returns {Always}
249
     */
250
    Always.prototype.notifyRemoved = function (element) {
251
        // we need to manually cascade notify all child nodes as the observer won't do it automatically
252
        if (element.children) {
253
            for (var _i = 0, _a = element.children; _i < _a.length; _i++) {
254
                var node = _a[_i];
255
                this.notifyRemoved(node);
256
            }
257
        }
258
        // callbacks for removals are invoked for deepest children first
259
        this.notifyCallbacks(element, this.removedCallbacks, AlwaysData.OPERATION_REMOVAL);
260
        return this;
261
    };
262
    /**
263
     * Adds a new inserted callback for the specified selector.
264
     *
265
     * @param {string} selector
266
     * @param {() => void} callback
267
     * @returns {Always}
268
     */
269
    Always.prototype.addInsertedCallback = function (selector, callback) {
270
        return this.addCallback(this.insertedCallbacks, selector, callback);
271
    };
272
    /**
273
     * Adds a new removed callback for the specified selector.
274
     *
275
     * @param {string} selector
276
     * @param {() => void} callback
277
     * @returns {Always}
278
     */
279
    Always.prototype.addRemovedCallback = function (selector, callback) {
280
        return this.addCallback(this.removedCallbacks, selector, callback);
281
    };
282
    /**
283
     * Removes the specified inserted callback for the specified selector.
284
     * If no callback is specified, removes all callbacks for the selector.
285
     *
286
     * @param {string} selector
287
     * @param {() => void} callback
288
     * @returns {Always}
289
     */
290
    Always.prototype.removeInsertedCallback = function (selector, callback) {
291
        return this.removeCallback(this.insertedCallbacks, selector, callback);
292
    };
293
    /**
294
     * Removes the specified removed callback for the specified selector.
295
     * If no callback is specified, removes all callbacks for the selector.
296
     *
297
     * @param {string} selector
298
     * @param {() => void} callback
299
     * @returns {Always}
300
     */
301
    Always.prototype.removeRemovedCallback = function (selector, callback) {
302
        return this.removeCallback(this.removedCallbacks, selector, callback);
303
    };
304
    return Always;
305
}());
306
307
function bindingNative() {
308
    window.Always = {
309
        always: Always.always,
310
        never: Always.never
311
    };
312
}
313
314
function bindingJQuery() {
315
    if ('undefined' === typeof jQuery) {
316
        return;
317
    }
318
    (function ($) {
319
        $.extend($.fn, {
320
            always: function (selector, onInserted, onRemoved) {
321
                return $(this).each(function () {
322
                    Always.always(this, selector, onInserted, onRemoved);
323
                });
324
            },
325
            never: function (selector, onInserted, onRemoved) {
326
                return $(this).each(function () {
327
                    Always.never(this, selector, onInserted, onRemoved);
328
                });
329
            }
330
        });
331
    })(jQuery);
332
}
333
334
var Bindings = /** @class */ (function () {
335
    function Bindings() {
336
    }
337
    Bindings.native = bindingNative;
338
    Bindings.jQuery = bindingJQuery;
339
    return Bindings;
340
}());
341
342
function polyfillMutationObserver() {
343
    if (window.MutationObserver) {
344
        return;
345
    }
346
    window.MutationObserver = window.WebKitMutationObserver;
347
}
348
349
function polyfillElementMatches() {
350
    var _this = this;
351
    var prototype = Element.prototype;
352
    if (prototype.matches) {
353
        return;
354
    }
355
    prototype.matches =
356
        prototype.matchesSelector ||
357
            prototype.mozMatchesSelector ||
358
            prototype.msMatchesSelector ||
359
            prototype.oMatchesSelector ||
360
            prototype.webkitMatchesSelector ||
361
            (function (s) {
362
                var matches = (_this.document || _this.ownerDocument).querySelectorAll(s);
363
                for (var i = 0; i < matches.length; i++) {
364
                    if (matches.item(i) === _this) {
365
                        return true;
366
                    }
367
                }
368
                return false;
369
            });
370
}
371
372
var Polyfills = /** @class */ (function () {
373
    function Polyfills() {
374
    }
375
    Polyfills.elementMatches = polyfillElementMatches;
376
    Polyfills.mutationObserver = polyfillMutationObserver;
377
    return Polyfills;
378
}());
379
380
/** global: Element */
381
/** global: HTMLElement */
382
/** global: MutationObserver */
383
/** global: WebKitMutationObserver */
384
// polyfills
385
Polyfills.elementMatches();
386
Polyfills.mutationObserver();
387
// bindings
388
Bindings.native();
389
Bindings.jQuery();
390
391
}(jQuery));
392