Passed
Push — master ( 364324...1b37e1 )
by Tony
01:28
created

Always.notifyCallbacks   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
nc 2
dl 0
loc 19
rs 9.4285
nop 3
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
            [].forEach.call(element.querySelectorAll(selector), function (node) {
117
                onInserted.call(node);
118
            });
119
        }
120
        // register removed callbacks
121
        if ('function' === typeof onRemoved) {
122
            instance.addRemovedCallback(selector, onRemoved);
123
        }
124
    };
125
    /**
126
     * Detaches the specified inserted / removed listener(s) for elements matching the specified selector on the
127
     * specified element as parent.
128
     *
129
     * @param {HTMLElement} element
130
     * @param {string} selector
131
     * @param {() => void} onInserted
132
     * @param {() => void} onRemoved
133
     */
134
    Always.never = function (element, selector, onInserted, onRemoved) {
135
        // if no selector is specified, quickest way to remove all listeners is to just detach
136
        if (!selector) {
137
            Always.detach(element);
138
            return;
139
        }
140
        var instance = Always.attach(element);
141
        // if no specific callback is requested, remove all listeners that match the selector
142
        if (!onInserted && !onRemoved) {
143
            instance.removeInsertedCallback(selector);
144
            instance.removeRemovedCallback(selector);
145
            return;
146
        }
147
        // remove only specific listeners
148
        if (onInserted) {
149
            instance.removeInsertedCallback(selector, onInserted);
150
        }
151
        if (onRemoved) {
152
            instance.removeRemovedCallback(selector, onRemoved);
153
        }
154
    };
155
    /**
156
     * Adds a new callback for the specified selector.
157
     *
158
     * @param {{[p: string]: (() => void)[]}} callbacks
159
     * @param {string} selector
160
     * @param {() => void} callback
161
     * @returns {Always}
162
     */
163
    Always.prototype.addCallback = function (callbacks, selector, callback) {
164
        selector = Always.normalizeSelector(selector);
165
        if (!callbacks.hasOwnProperty(selector)) {
166
            callbacks[selector] = [];
167
        }
168
        callbacks[selector].push(callback);
169
        return this;
170
    };
171
    /**
172
     * Removes the specified callback for the specified selector.
173
     * If no callback is specified, removes all callbacks for the selector.
174
     *
175
     * @param {{[p: string]: (() => void)[]}} callbacks
176
     * @param {string} selector
177
     * @param {() => void} callback
178
     * @returns {Always}
179
     */
180
    Always.prototype.removeCallback = function (callbacks, selector, callback) {
181
        selector = Always.normalizeSelector(selector);
182
        if (!callbacks.hasOwnProperty(selector)) {
183
            return this;
184
        }
185
        if (callback) {
186
            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...
187
                callbacks[selector].splice(index, 1);
188
            }
189
        }
190
        else {
191
            delete callbacks[selector];
192
        }
193
        return this;
194
    };
195
    /**
196
     * Notifies the specified callbacks for the specified operation on the specified element.
197
     * This is a convenience method for notifyInserted and notifyRemoved.
198
     *
199
     * @param {HTMLElement} element
200
     * @param {{[p: string]: (() => void)[]}} callbacks
201
     * @param {number} operation
202
     * @returns {Always}
203
     */
204
    Always.prototype.notifyCallbacks = function (element, callbacks, operation) {
205
        // make the element manageable & prevent duplicate invocations
206
        // even making every single node in the Dom manageable is still much faster that matching it against a selector
207
        var data = Always.data(element);
208
        if (operation === data.lastOperation) {
209
            return this;
210
        }
211
        // traverse requested callbacks & invoke those for which the element matches the corresponding selector
212
        // also update the AlwaysData.lastOperation to prevent future duplicate invocations
213
        Object.keys(callbacks).forEach(function (selector) {
214
            if (element.matches(selector)) {
215
                callbacks[selector].forEach(function (callback) {
216
                    data.lastOperation = operation;
217
                    callback.call(element);
218
                });
219
            }
220
        });
221
        return this;
222
    };
223
    /**
224
     * Notifies all registered callbacks about an insertion, if the corresponding selector matches the node.
225
     *
226
     * @param {HTMLElement} element
227
     * @returns {Always}
228
     */
229
    Always.prototype.notifyInserted = function (element) {
230
        var _this = this;
231
        // callbacks for insertions are invoked for parents first
232
        this.notifyCallbacks(element, this.insertedCallbacks, AlwaysData.OPERATION_INSERTION);
233
        // we need to manually cascade notify all child nodes as the observer won't do it automatically
234
        [].forEach.call(element.children, function (node) {
235
            _this.notifyInserted(node);
236
        });
237
        return this;
238
    };
239
    /**
240
     * Notifies all registered callbacks about a removal, if the corresponding selector matches the node.
241
     *
242
     * @param {HTMLElement} element
243
     * @returns {Always}
244
     */
245
    Always.prototype.notifyRemoved = function (element) {
246
        var _this = this;
247
        // we need to manually cascade notify all child nodes as the observer won't do it automatically
248
        [].forEach.call(element.children, function (node) {
249
            _this.notifyRemoved(node);
250
        });
251
        // callbacks for removals are invoked for deepest children first
252
        this.notifyCallbacks(element, this.removedCallbacks, AlwaysData.OPERATION_REMOVAL);
253
        return this;
254
    };
255
    /**
256
     * Adds a new inserted callback for the specified selector.
257
     *
258
     * @param {string} selector
259
     * @param {() => void} callback
260
     * @returns {Always}
261
     */
262
    Always.prototype.addInsertedCallback = function (selector, callback) {
263
        return this.addCallback(this.insertedCallbacks, selector, callback);
264
    };
265
    /**
266
     * Adds a new removed callback for the specified selector.
267
     *
268
     * @param {string} selector
269
     * @param {() => void} callback
270
     * @returns {Always}
271
     */
272
    Always.prototype.addRemovedCallback = function (selector, callback) {
273
        return this.addCallback(this.removedCallbacks, selector, callback);
274
    };
275
    /**
276
     * Removes the specified inserted callback for the specified selector.
277
     * If no callback is specified, removes all callbacks for the selector.
278
     *
279
     * @param {string} selector
280
     * @param {() => void} callback
281
     * @returns {Always}
282
     */
283
    Always.prototype.removeInsertedCallback = function (selector, callback) {
284
        return this.removeCallback(this.insertedCallbacks, selector, callback);
285
    };
286
    /**
287
     * Removes the specified removed callback for the specified selector.
288
     * If no callback is specified, removes all callbacks for the selector.
289
     *
290
     * @param {string} selector
291
     * @param {() => void} callback
292
     * @returns {Always}
293
     */
294
    Always.prototype.removeRemovedCallback = function (selector, callback) {
295
        return this.removeCallback(this.removedCallbacks, selector, callback);
296
    };
297
    return Always;
298
}());
299
300
function bindingNative() {
301
    window.Always = {
302
        always: Always.always,
303
        never: Always.never
304
    };
305
}
306
307
function bindingJQuery() {
308
    if ('undefined' === typeof jQuery) {
309
        return;
310
    }
311
    (function ($) {
312
        $.extend($.fn, {
313
            always: function (selector, onInserted, onRemoved) {
314
                return $(this).each(function () {
315
                    Always.always(this, selector, onInserted, onRemoved);
316
                });
317
            },
318
            never: function (selector, onInserted, onRemoved) {
319
                return $(this).each(function () {
320
                    Always.never(this, selector, onInserted, onRemoved);
321
                });
322
            }
323
        });
324
    })(jQuery);
325
}
326
327
var Bindings = /** @class */ (function () {
328
    function Bindings() {
329
    }
330
    Bindings.native = bindingNative;
331
    Bindings.jQuery = bindingJQuery;
332
    return Bindings;
333
}());
334
335
function polyfillMutationObserver() {
336
    if (window.MutationObserver) {
337
        return;
338
    }
339
    window.MutationObserver = window.WebKitMutationObserver;
340
}
341
342
function polyfillElementMatches() {
343
    var _this = this;
344
    var prototype = Element.prototype;
345
    if (prototype.matches) {
346
        return;
347
    }
348
    prototype.matches =
349
        prototype.matchesSelector ||
350
            prototype.mozMatchesSelector ||
351
            prototype.msMatchesSelector ||
352
            prototype.oMatchesSelector ||
353
            prototype.webkitMatchesSelector ||
354
            (function (s) {
355
                var matches = (_this.document || _this.ownerDocument).querySelectorAll(s);
356
                for (var i = 0; i < matches.length; i++) {
357
                    if (matches.item(i) === _this) {
358
                        return true;
359
                    }
360
                }
361
                return false;
362
            });
363
}
364
365
var Polyfills = /** @class */ (function () {
366
    function Polyfills() {
367
    }
368
    Polyfills.elementMatches = polyfillElementMatches;
369
    Polyfills.mutationObserver = polyfillMutationObserver;
370
    return Polyfills;
371
}());
372
373
/** global: Element */
374
/** global: HTMLElement */
375
/** global: MutationObserver */
376
/** global: WebKitMutationObserver */
377
// polyfills
378
Polyfills.elementMatches();
379
Polyfills.mutationObserver();
380
// bindings
381
Bindings.native();
382
Bindings.jQuery();
383
384
}(jQuery));
385