PageSourceSearch

https://actionnetwork.org/includes/js/mutation_polyfill.js?v=1

js actionnetwork.org collected 2026-09-24 07:03:42 UTC 24,572 bytes, 610 lines download raw bytes

vendor: 24,572 bytes, lines 1-610
1/*!
2 * Shim for MutationObserver interface
3 * Author: Graeme Yeates (github.com/megawac)
4 * Repository: https://github.com/megawac/MutationObserver.js
5 * License: WTFPL V2, 2004 (wtfpl.net).
6 * Though credit and staring the repo will make me feel pretty, you can modify and redistribute as you please.
7 * Attempts to follow spec (http:// www.w3.org/TR/dom/#mutation-observers) as closely as possible for native javascript
8 * See https://github.com/WebKit/webkit/blob/master/Source/WebCore/dom/MutationObserver.cpp for current webkit source c++ implementation
9 */
10
11/**
12 * prefix bugs:
13    - https://bugs.webkit.org/show_bug.cgi?id=85161
14    - https://bugzilla.mozilla.org/show_bug.cgi?id=749920
15 * Don't use WebKitMutationObserver as Safari (6.0.5-6.1) use a buggy implementation
16*/
17window.MutationObserver = window.MutationObserver || (function(undefined) {
18    "use strict";
19    /**
20     * @param {function(Array.<MutationRecord>, MutationObserver)} listener
21     * @constructor
22     */
23    function MutationObserver(listener) {
24        /**
25         * @type {Array.<Object>}
26         * @private
27         */
28        this._watched = [];
29        /** @private */
30        this._listener = listener;
31    }
32
33    /**
34     * Start a recursive timeout function to check all items being observed for mutations
35     * @type {MutationObserver} observer
36     * @private
37     */
38    function startMutationChecker(observer) {
39        (function check() {
40            var mutations = observer.takeRecords();
41
42            if (mutations.length) { // fire away
43                // calling the listener with context is not spec but currently consistent with FF and WebKit
44                observer._listener(mutations, observer);
45            }
46            /** @private */
47            observer._timeout = setTimeout(check, MutationObserver._period);
48        })();
49    }
50
51    /**
52     * Period to check for mutations (~32 times/sec)
53     * @type {number}
54     * @expose
55     */
56    MutationObserver._period = 30 /*ms+runtime*/ ;
57
58    /**
59     * Exposed API
60     * @expose
61     * @final
62     */
63    MutationObserver.prototype = {
64        /**
65         * see http:// dom.spec.whatwg.org/#dom-mutationobserver-observe
66         * not going to throw here but going to follow the current spec config sets
67         * @param {Node|null} $target
68         * @param {Object|null} config : MutationObserverInit configuration dictionary
69         * @expose
70         * @return undefined
71         */
72        observe: function($target, config) {
73            /**
74             * Using slightly different names so closure can go ham
75             * @type {!Object} : A custom mutation config
76             */
77            var settings = {
78                attr: !! (config.attributes || config.attributeFilter || config.attributeOldValue),
79
80                // some browsers enforce that subtree must be set with childList, attributes or characterData.
81                // We don't care as spec doesn't specify this rule.
82                kids: !! config.childList,
83                descendents: !! config.subtree,
84                charData: !! (config.characterData || config.characterDataOldValue)
85            };
86
87            var watched = this._watched;
88
89            // remove already observed target element from pool
90            for (var i = 0; i < watched.length; i++) {
91                if (watched[i].tar === $target) watched.splice(i, 1);
92            }
93
94            if (config.attributeFilter) {
95                /**
96                 * converts to a {key: true} dict for faster lookup
97                 * @type {Object.<String,Boolean>}
98                 */
99                settings.afilter = reduce(config.attributeFilter, function(a, b) {
100                    a[b] = true;
101                    return a;
102                }, {});
103            }
104
105            watched.push({
106                tar: $target,
107                fn: createMutationSearcher($target, settings)
108            });
109
110            // reconnect if not connected
111            if (!this._timeout) {
112                startMutationChecker(this);
113            }
114        },
115
116        /**
117         * Finds mutations since last check and empties the "record queue" i.e. mutations will only be found once
118         * @expose
119         * @return {Array.<MutationRecord>}
120         */
121        takeRecords: function() {
122            var mutations = [];
123            var watched = this._watched;
124
125            for (var i = 0; i < watched.length; i++) {
126                watched[i].fn(mutations);
127            }
128
129            return mutations;
130        },
131
132        /**
133         * @expose
134         * @return undefined
135         */
136        disconnect: function() {
137            this._watched = []; // clear the stuff being observed
138            clearTimeout(this._timeout); // ready for garbage collection
139            /** @private */
140            this._timeout = null;
141        }
142    };
143
144    /**
145     * Simple MutationRecord pseudoclass. No longer exposing as its not fully compliant
146     * @param {Object} data
147     * @return {Object} a MutationRecord
148     */
149    function MutationRecord(data) {
150        var settings = { // technically these should be on proto so hasOwnProperty will return false for non explicitly props
151            type: null,
152            target: null,
153            addedNodes: [],
154            removedNodes: [],
155            previousSibling: null,
156            nextSibling: null,
157            attributeName: null,
158            attributeNamespace: null,
159            oldValue: null
160        };
161        for (var prop in data) {
162            if (has(settings, prop) && data[prop] !== undefined) settings[prop] = data[prop];
163        }
164        return settings;
165    }
166
167    /**
168     * Creates a func to find all the mutations
169     *
170     * @param {Node} $target
171     * @param {!Object} config : A custom mutation config
172     */
173    function createMutationSearcher($target, config) {
174        /** type {Elestuct} */
175        var $oldstate = clone($target, config); // create the cloned datastructure
176
177        /**
178         * consumes array of mutations we can push to
179         *
180         * @param {Array.<MutationRecord>} mutations
181         */
182        return function(mutations) {
183            var olen = mutations.length, dirty;
184
185            // Alright we check base level changes in attributes... easy
186            if (config.attr && $oldstate.attr) {
187                findAttributeMutations(mutations, $target, $oldstate.attr, config.afilter);
188            }
189
190            // check childlist or subtree for mutations
191            if (config.kids || config.descendents) {
192                dirty = searchSubtree(mutations, $target, $oldstate, config);
193            }
194
195            // reclone data structure if theres changes
196            if (dirty || mutations.length !== olen) {
197                /** type {Elestuct} */
198                $oldstate = clone($target, config);
199            }
200        };
201    }
202
203    /* attributes + attributeFilter helpers */
204
205    // Check if the environment has the attribute bug (#4) which cause
206    // element.attributes.style to always be null.
207    var hasAttributeBug = document.createElement("i");
208    hasAttributeBug.style.top = 0;
209    hasAttributeBug = hasAttributeBug.attributes.style.value != "null";
210
211    /**
212     * Gets an attribute value in an environment without attribute bug
213     *
214     * @param {Node} el
215     * @param {Attr} attr
216     * @return {String} an attribute value
217     */
218    function getAttributeSimple(el, attr) {
219        // There is a potential for a warning to occur here if the attribute is a
220        // custom attribute in IE<9 with a custom .toString() method. This is
221        // just a warning and doesn't affect execution (see #21)
222        return attr.value;
223    }
224
225    /**
226     * Gets an attribute value with special hack for style attribute (see #4)
227     *
228     * @param {Node} el
229     * @param {Attr} attr
230     * @return {String} an attribute value
231     */
232    function getAttributeWithStyleHack(el, attr) {
233        // As with getAttributeSimple there is a potential warning for custom attribtues in IE7.
234        return attr.name !== "style" ? attr.value : el.style.cssText;
235    }
236
237    var getAttributeValue = hasAttributeBug ? getAttributeSimple : getAttributeWithStyleHack;
238
239    /**
240     * fast helper to check to see if attributes object of an element has changed
241     * doesnt handle the textnode case
242     *
243     * @param {Array.<MutationRecord>} mutations
244     * @param {Node} $target
245     * @param {Object.<string, string>} $oldstate : Custom attribute clone data structure from clone
246     * @param {Object} filter
247     */
248    function findAttributeMutations(mutations, $target, $oldstate, filter) {
249        var checked = {};
250        var attributes = $target.attributes;
251        var attr;
252        var name;
253        var i = attributes.length;
254        while (i--) {
255            attr = attributes[i];
256            name = attr.name;
257            if (!filter || has(filter, name)) {
258                if (getAttributeValue($target, attr) !== $oldstate[name]) {
259                    // The pushing is redundant but gzips very nicely
260                    mutations.push(MutationRecord({
261                        type: "attributes",
262                        target: $target,
263                        attributeName: name,
264                        oldValue: $oldstate[name],
265                        attributeNamespace: attr.namespaceURI // in ie<8 it incorrectly will return undefined
266                    }));
267                }
268                checked[name] = true;
269            }
270        }
271        for (name in $oldstate) {
272            if (!(checked[name])) {
273                mutations.push(MutationRecord({
274                    target: $target,
275                    type: "attributes",
276                    attributeName: name,
277                    oldValue: $oldstate[name]
278                }));
279            }
280        }
281    }
282
283    /**
284     * searchSubtree: array of mutations so far, element, element clone, bool
285     * synchronous dfs comparision of two nodes
286     * This function is applied to any observed element with childList or subtree specified
287     * Sorry this is kind of confusing as shit, tried to comment it a bit...
288     * codereview.stackexchange.com/questions/38351 discussion of an earlier version of this func
289     *
290     * @param {Array} mutations
291     * @param {Node} $target
292     * @param {!Object} $oldstate : A custom cloned node from clone()
293     * @param {!Object} config : A custom mutation config
294     */
295    function searchSubtree(mutations, $target, $oldstate, config) {
296        // Track if the tree is dirty and has to be recomputed (#14).
297        var dirty;
298        /*
299         * Helper to identify node rearrangment and stuff...
300         * There is no gaurentee that the same node will be identified for both added and removed nodes
301         * if the positions have been shuffled.
302         * conflicts array will be emptied by end of operation
303         */
304        function resolveConflicts(conflicts, node, $kids, $oldkids, numAddedNodes) {
305            // the distance between the first conflicting node and the last
306            var distance = conflicts.length - 1;
307            // prevents same conflict being resolved twice consider when two nodes switch places.
308            // only one should be given a mutation event (note -~ is used as a math.ceil shorthand)
309            var counter = -~((distance - numAddedNodes) / 2);
310            var $cur;
311            var oldstruct;
312            var conflict;
313            while ((conflict = conflicts.pop())) {
314                $cur = $kids[conflict.i];
315                oldstruct = $oldkids[conflict.j];
316
317                // attempt to determine if there was node rearrangement... won't gaurentee all matches
318                // also handles case where added/removed nodes cause nodes to be identified as conflicts
319                if (config.kids && counter && Math.abs(conflict.i - conflict.j) >= distance) {
320                    mutations.push(MutationRecord({
321                        type: "childList",
322                        target: node,
323                        addedNodes: [$cur],
324                        removedNodes: [$cur],
325                        // haha don't rely on this please
326                        nextSibling: $cur.nextSibling,
327                        previousSibling: $cur.previousSibling
328                    }));
329                    counter--; // found conflict
330                }
331
332                // Alright we found the resorted nodes now check for other types of mutations
333                if (config.attr && oldstruct.attr) findAttributeMutations(mutations, $cur, oldstruct.attr, config.afilter);
334                if (config.charData && $cur.nodeType === 3 && $cur.nodeValue !== oldstruct.charData) {
335                    mutations.push(MutationRecord({
336                        type: "characterData",
337                        target: $cur
338                    }));
339                }
340                // now look @ subtree
341                if (config.descendents) findMutations($cur, oldstruct);
342            }
343        }
344
345        /**
346         * Main worker. Finds and adds mutations if there are any
347         * @param {Node} node
348         * @param {!Object} old : A cloned data structure using internal clone
349         */
350        function findMutations(node, old) {
351            var $kids = node.childNodes;
352            var $oldkids = old.kids;
353            var klen = $kids.length;
354            // $oldkids will be undefined for text and comment nodes
355            var olen = $oldkids ? $oldkids.length : 0;
356            // if (!olen && !klen) return; // both empty; clearly no changes
357
358            // we delay the intialization of these for marginal performance in the expected case (actually quite signficant on large subtrees when these would be otherwise unused)
359            // map of checked element of ids to prevent registering the same conflict twice
360            var map;
361            // array of potential conflicts (ie nodes that may have been re arranged)
362            var conflicts;
363            var id; // element id from getElementId helper
364            var idx; // index of a moved or inserted element
365
366            var oldstruct;
367            // current and old nodes
368            var $cur;
369            var $old;
370            // track the number of added nodes so we can resolve conflicts more accurately
371            var numAddedNodes = 0;
372
373            // iterate over both old and current child nodes at the same time
374            var i = 0, j = 0;
375            // while there is still anything left in $kids or $oldkids (same as i < $kids.length || j < $oldkids.length;)
376            while( i < klen || j < olen ) {
377                // current and old nodes at the indexs
378                $cur = $kids[i];
379                oldstruct = $oldkids[j];
380                $old = oldstruct && oldstruct.node;
381
382                if ($cur === $old) { // expected case - optimized for this case
383                    // check attributes as specified by config
384                    if (config.attr && oldstruct.attr) /* oldstruct.attr instead of textnode check */findAttributeMutations(mutations, $cur, oldstruct.attr, config.afilter);
385                    // check character data if node is a comment or textNode and it's being observed
386                    if (config.charData && oldstruct.charData !== undefined && $cur.nodeValue !== oldstruct.charData) {
387                        mutations.push(MutationRecord({
388                            type: "characterData",
389                            target: $cur
390                        }));
391                    }
392
393                    // resolve conflicts; it will be undefined if there are no conflicts - otherwise an array
394                    if (conflicts) resolveConflicts(conflicts, node, $kids, $oldkids, numAddedNodes);
395
396                    // recurse on next level of children. Avoids the recursive call when there are no children left to iterate
397                    if (config.descendents && ($cur.childNodes.length || oldstruct.kids && oldstruct.kids.length)) findMutations($cur, oldstruct);
398
399                    i++;
400                    j++;
401                } else { // (uncommon case) lookahead until they are the same again or the end of children
402                    dirty = true;
403                    if (!map) { // delayed initalization (big perf benefit)
404                        map = {};
405                        conflicts = [];
406                    }
407                    if ($cur) {
408                        // check id is in the location map otherwise do a indexOf search
409                        if (!(map[id = getElementId($cur)])) { // to prevent double checking
410                            // mark id as found
411                            map[id] = true;
412                            // custom indexOf using comparitor checking oldkids[i].node === $cur
413                            if ((idx = indexOfCustomNode($oldkids, $cur, j)) === -1) {
414                                if (config.kids) {
415                                    mutations.push(MutationRecord({
416                                        type: "childList",
417                                        target: node,
418                                        addedNodes: [$cur], // $cur is a new node
419                                        nextSibling: $cur.nextSibling,
420                                        previousSibling: $cur.previousSibling
421                                    }));
422                                    numAddedNodes++;
423                                }
424                            } else {
425                                conflicts.push({ // add conflict
426                                    i: i,
427                                    j: idx
428                                });
429                            }
430                        }
431                        i++;
432                    }
433
434                    if ($old &&
435                       // special case: the changes may have been resolved: i and j appear congurent so we can continue using the expected case
436                       $old !== $kids[i]
437                    ) {
438                        if (!(map[id = getElementId($old)])) {
439                            map[id] = true;
440                            if ((idx = indexOf($kids, $old, i)) === -1) {
441                                if (config.kids) {
442                                    mutations.push(MutationRecord({
443                                        type: "childList",
444                                        target: old.node,
445                                        removedNodes: [$old],
446                                        nextSibling: $oldkids[j + 1], // praise no indexoutofbounds exception
447                                        previousSibling: $oldkids[j - 1]
448                                    }));
449                                    numAddedNodes--;
450                                }
451                            } else {
452                                conflicts.push({
453                                    i: idx,
454                                    j: j
455                                });
456                            }
457                        }
458                        j++;
459                    }
460                }// end uncommon case
461            }// end loop
462
463            // resolve any remaining conflicts
464            if (conflicts) resolveConflicts(conflicts, node, $kids, $oldkids, numAddedNodes);
465        }
466        findMutations($target, $oldstate);
467        return dirty;
468    }
469
470    /**
471     * Utility
472     * Cones a element into a custom data structure designed for comparision. https://gist.github.com/megawac/8201012
473     *
474     * @param {Node} $target
475     * @param {!Object} config : A custom mutation config
476     * @return {!Object} : Cloned data structure
477     */
478    function clone($target, config) {
479        var recurse = true; // set true so childList we'll always check the first level
480        return (function copy($target) {
481            var elestruct = {
482                /** @type {Node} */
483                node: $target
484            };
485
486            // Store current character data of target text or comment node if the config requests
487            // those properties to be observed.
488            if (config.charData && ($target.nodeType === 3 || $target.nodeType === 8)) {
489                elestruct.charData = $target.nodeValue;
490            }
491            // its either a element, comment, doc frag or document node
492            else {
493                // Add attr only if subtree is specified or top level and avoid if
494                // attributes is a document object (#13).
495                if (config.attr && recurse && $target.nodeType === 1) {
496                    /**
497                     * clone live attribute list to an object structure {name: val}
498                     * @type {Object.<string, string>}
499                     */
500                    elestruct.attr = reduce($target.attributes, function(memo, attr) {
501                        if (!config.afilter || config.afilter[attr.name]) {
502                            memo[attr.name] = getAttributeValue($target, attr);
503                        }
504                        return memo;
505                    }, {});
506                }
507
508                // whether we should iterate the children of $target node
509                if (recurse && ((config.kids || config.charData) || (config.attr && config.descendents)) ) {
510                    /** @type {Array.<!Object>} : Array of custom clone */
511                    elestruct.kids = map($target.childNodes, copy);
512                }
513
514                recurse = config.descendents;
515            }
516            return elestruct;
517        })($target);
518    }
519
520    /**
521     * indexOf an element in a collection of custom nodes
522     *
523     * @param {NodeList} set
524     * @param {!Object} $node : A custom cloned node
525     * @param {number} idx : index to start the loop
526     * @return {number}
527     */
528    function indexOfCustomNode(set, $node, idx) {
529        return indexOf(set, $node, idx, JSCompiler_renameProperty("node"));
530    }
531
532    // using a non id (eg outerHTML or nodeValue) is extremely naive and will run into issues with nodes that may appear the same like <li></li>
533    var counter = 1; // don't use 0 as id (falsy)
534    /** @const */
535    var expando = "mo_id";
536
537    /**
538     * Attempt to uniquely id an element for hashing. We could optimize this for legacy browsers but it hopefully wont be called enough to be a concern
539     *
540     * @param {Node} $ele
541     * @return {(string|number)}
542     */
543    function getElementId($ele) {
544        try {
545            return $ele.id || ($ele[expando] = $ele[expando] || counter++);
546        } catch (o_O) { // ie <8 will throw if you set an unknown property on a text node
547            try {
548                return $ele.nodeValue; // naive
549            } catch (shitie) { // when text node is removed: https://gist.github.com/megawac/8355978 :(
550                return counter++;
551            }
552        }
553    }
554
555    /**
556     * **map** Apply a mapping function to each item of a set
557     * @param {Array|NodeList} set
558     * @param {Function} iterator
559     */
560    function map(set, iterator) {
561        var results = [];
562        for (var index = 0; index < set.length; index++) {
563            results[index] = iterator(set[index], index, set);
564        }
565        return results;
566    }
567
568    /**
569     * **Reduce** builds up a single result from a list of values
570     * @param {Array|NodeList|NamedNodeMap} set
571     * @param {Function} iterator
572     * @param {*} [memo] Initial value of the memo.
573     */
574    function reduce(set, iterator, memo) {
575        for (var index = 0; index < set.length; index++) {
576            memo = iterator(memo, set[index], index, set);
577        }
578        return memo;
579    }
580
581    /**
582     * **indexOf** find index of item in collection.
583     * @param {Array|NodeList} set
584     * @param {Object} item
585     * @param {number} idx
586     * @param {string} [prop] Property on set item to compare to item
587     */
588    function indexOf(set, item, idx, prop) {
589        for (/*idx = ~~idx*/; idx < set.length; idx++) {// start idx is always given as this is internal
590            if ((prop ? set[idx][prop] : set[idx]) === item) return idx;
591        }
592        return -1;
593    }
594
595    /**
596     * @param {Object} obj
597     * @param {(string|number)} prop
598     * @return {boolean}
599     */
600    function has(obj, prop) {
601        return obj[prop] !== undefined; // will be nicely inlined by gcc
602    }
603
604    // GCC hack see http:// stackoverflow.com/a/23202438/1517919
605    function JSCompiler_renameProperty(a) {
606        return a;
607    }
608
609    return MutationObserver;
610})(void 0);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.