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

always.js (5 issues)

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)) {
0 ignored issues
show
The variable HTMLElement seems to be never declared. If this is a global, consider adding a /** global: HTMLElement */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
32
                    return;
33
                }
34
                _this.notifyInserted(node);
35
            });
36
            [].forEach.call(mutation.removedNodes, function (node) {
37
                if (!(node instanceof HTMLElement)) {
0 ignored issues
show
The variable HTMLElement seems to be never declared. If this is a global, consider adding a /** global: HTMLElement */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
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
            var index = void 0;
0 ignored issues
show
Consider using undefined instead of void(0). It is equivalent and more straightforward to read.
Loading history...
The assignment to variable index seems to be never used. Consider removing it.
Loading history...
185
            while (-1 < (index = callbacks[selector].indexOf(callback))) {
186
                callbacks[selector].splice(index, 1);
187
            }
188
        }
189
        else {
190
            delete callbacks[selector];
191
        }
192
        return this;
193
    };
194
    /**
195
     * Notifies all registered callbacks about an insertion, if the corresponding selector matches the node.
196
     *
197
     * @param {HTMLElement} element
198
     * @returns {Always}
199
     */
200
    Always.prototype.notifyInserted = function (element) {
201
        var _this = this;
202
        // ignore duplicate invocations
203
        var data = Always.data(element);
204
        if (AlwaysData.OPERATION_INSERTION === data.lastOperation) {
205
            return this;
206
        }
207
        data.lastOperation = AlwaysData.OPERATION_INSERTION;
208
        Object.keys(this.insertedCallbacks).forEach(function (selector) {
209
            if (element.matches(selector)) {
210
                _this.insertedCallbacks[selector].forEach(function (callback) {
211
                    callback.call(element);
212
                });
213
            }
214
        });
215
        // we need to manually cascade notify all child nodes as the observer won't do it automatically
216
        [].forEach.call(element.querySelectorAll('*'), function (node) {
217
            _this.notifyInserted(node);
218
        });
219
        return this;
220
    };
221
    /**
222
     * Notifies all registered callbacks about a removal, if the corresponding selector matches the node.
223
     *
224
     * @param {HTMLElement} element
225
     * @returns {Always}
226
     */
227
    Always.prototype.notifyRemoved = function (element) {
228
        var _this = this;
229
        // ignore duplicate invocations
230
        var data = Always.data(element);
231
        if (AlwaysData.OPERATION_REMOVAL === data.lastOperation) {
232
            return this;
233
        }
234
        data.lastOperation = AlwaysData.OPERATION_REMOVAL;
235
        // we need to manually cascade notify all child nodes as the observer won't do it automatically
236
        [].forEach.call(element.querySelectorAll('*'), function (node) {
237
            _this.notifyRemoved(node);
238
        });
239
        Object.keys(this.removedCallbacks).forEach(function (selector) {
240
            if (element.matches(selector)) {
241
                _this.removedCallbacks[selector].forEach(function (callback) {
242
                    callback.call(element);
243
                });
244
            }
245
        });
246
        return this;
247
    };
248
    /**
249
     * Adds a new inserted callback for the specified selector.
250
     *
251
     * @param {string} selector
252
     * @param {() => void} callback
253
     * @returns {Always}
254
     */
255
    Always.prototype.addInsertedCallback = function (selector, callback) {
256
        return this.addCallback(this.insertedCallbacks, selector, callback);
257
    };
258
    /**
259
     * Adds a new removed callback for the specified selector.
260
     *
261
     * @param {string} selector
262
     * @param {() => void} callback
263
     * @returns {Always}
264
     */
265
    Always.prototype.addRemovedCallback = function (selector, callback) {
266
        return this.addCallback(this.removedCallbacks, selector, callback);
267
    };
268
    /**
269
     * Removes the specified inserted callback for the specified selector.
270
     * If no callback is specified, removes all callbacks for the selector.
271
     *
272
     * @param {string} selector
273
     * @param {() => void} callback
274
     * @returns {Always}
275
     */
276
    Always.prototype.removeInsertedCallback = function (selector, callback) {
277
        return this.removeCallback(this.insertedCallbacks, selector, callback);
278
    };
279
    /**
280
     * Removes the specified removed callback for the specified selector.
281
     * If no callback is specified, removes all callbacks for the selector.
282
     *
283
     * @param {string} selector
284
     * @param {() => void} callback
285
     * @returns {Always}
286
     */
287
    Always.prototype.removeRemovedCallback = function (selector, callback) {
288
        return this.removeCallback(this.removedCallbacks, selector, callback);
289
    };
290
    return Always;
291
}());
292
293
function bindingNative() {
294
    window.Always = {
295
        always: Always.always,
296
        never: Always.never
297
    };
298
}
299
300
function bindingJQuery() {
301
    if ('undefined' === typeof jQuery) {
302
        return;
303
    }
304
    (function ($) {
305
        $.extend($.fn, {
306
            always: function (selector, onInserted, onRemoved) {
307
                return $(this).each(function () {
308
                    Always.always(this, selector, onInserted, onRemoved);
309
                });
310
            },
311
            never: function (selector, onInserted, onRemoved) {
312
                return $(this).each(function () {
313
                    Always.never(this, selector, onInserted, onRemoved);
314
                });
315
            }
316
        });
317
    })(jQuery);
318
}
319
320
var Bindings = /** @class */ (function () {
321
    function Bindings() {
322
    }
323
    Bindings.native = bindingNative;
324
    Bindings.jQuery = bindingJQuery;
325
    return Bindings;
326
}());
327
328
function polyfillMutationObserver() {
329
    /** global: MutationObserver */
330
    /** global: WebKitMutationObserver */
331
    if (window.MutationObserver) {
332
        return;
333
    }
334
    window.MutationObserver = window.WebKitMutationObserver;
335
}
336
337
function polyfillElementMatches() {
338
    /** global: Element */
339
    var _this = this;
340
    var prototype = Element.prototype;
341
    if (prototype.matches) {
342
        return;
343
    }
344
    prototype.matches =
345
        prototype.matchesSelector ||
346
            prototype.mozMatchesSelector ||
347
            prototype.msMatchesSelector ||
348
            prototype.oMatchesSelector ||
349
            prototype.webkitMatchesSelector ||
350
            (function (s) {
351
                var matches = (_this.document || _this.ownerDocument).querySelectorAll(s), i = matches.length;
352
                // continue until one of the items matches this
353
                while (--i >= 0 && matches.item(i) !== _this) { }
0 ignored issues
show
Comprehensibility Documentation Best Practice introduced by
This code block is empty. Consider removing it or adding a comment to explain.
Loading history...
354
                return i > -1;
355
            });
356
}
357
358
var Polyfills = /** @class */ (function () {
359
    function Polyfills() {
360
    }
361
    Polyfills.elementMatches = polyfillElementMatches;
362
    Polyfills.mutationObserver = polyfillMutationObserver;
363
    return Polyfills;
364
}());
365
366
// polyfills
367
Polyfills.elementMatches();
368
Polyfills.mutationObserver();
369
// bindings
370
Bindings.native();
371
Bindings.jQuery();
372
373
}(jQuery));
374