Passed
Push — master ( f45dbd...364324 )
by Tony
01:15
created

always.js (1 issue)

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