1

In a userscript, I do lots of detection of element changes using waitForKeyElements().
However, I've run into a specific example where waitForKeyElements doesn't reliably fire on change.
My userscript adjusts theirID to insert the internal span, such as you see here:

<span id='theirID' class='myclass'>
  <span class='myclass2'>text</span> more text
</span>

The site then changes this code to erase my internal span, so that it then looks like:

<span id='theirID' class='myclass'>
  some text
</span>

For some reason, on some cases, waitForKeyElements just doesn't get triggered on that change. I've tried to detect changes on the outer span, the inner span, the outer span's parent, its parent, etc.

So I'm now wondering if there's some other way to detect, for example, whether the element w/ myclass2 (the inner span) has vanished? I could poll $('.myclass2').length I suppose, but I wouldn't know where in the document it vanished from. I'd ideally know the specific parent that held the now-missing item.

Any ideas?

Brock Adams
  • 90,639
  • 22
  • 233
  • 295
mix
  • 6,943
  • 15
  • 61
  • 90
  • Are you removing the DOM elements yourself, or is there some other process that you have no control over removing the elements? – Steven Hunt Sep 25 '12 at 19:10
  • I have no control over it. It's the host site making the change. They're unaware of me, and are just rewriting the inner contents of the outer span I suspect. – mix Sep 25 '12 at 19:11
  • Take a look at this answer, I'll bet it has the solution you are looking for: [http://stackoverflow.com/questions/2844565/is-there-a-jquery-dom-change-listener](http://stackoverflow.com/questions/2844565/is-there-a-jquery-dom-change-listener) – weexpectedTHIS Sep 25 '12 at 19:13
  • interesting. DOMSubtreeModified does fire for the parent (outer span in my example above), although it fires many times on what appears to be a single change. it doesn't fire at all when attached to the vanishing inner span (myclass2 above). – mix Sep 25 '12 at 22:33
  • Don't use `DOMSubtreeModified`! It's buggy and **deprecated**. I'll post alternatives in a bit. Also, there is a way for `waitForKeyElements` to just continuously monitor node(s); the default behavior is it only fires once per each node that is found. – Brock Adams Sep 26 '12 at 00:09
  • `waitForKeyElements` is principally for Firefox Greasemonkey (although it can be shoehorned to work with Chrome, and probably works with Tampermonkey). **What browser are you using?** (Question tags may need adjusting.) – Brock Adams Sep 26 '12 at 02:53
  • developing in chrome, but planning to release in firefox (GM) and safari (extension) as well. until now it's been flawless for me in all 3. still stumped as to what's going on...i tried everything i could think of. for now my hack is to run a setInterval() that polls the same selectors and checks their contents to see if they've changed. – mix Sep 26 '12 at 06:50
  • @BrockAdams yea, i read that about DOMSubtreeModified. on waitForKeyElements, are you talking about the bWaitOnce parm? if so, yes, i'm using false on that to give me a continuous poll. – mix Sep 26 '12 at 06:51

1 Answers1

2

Here are two answers, one for this specific case (you're using waitForKeyElements, and another for the general case.

For this specific case:

Since you are using waitForKeyElements() already, you can take advantage of the return value in the node handler.

Suppose the target page originally had HTML like this:

<span id='theirID1' class='myclass'>
    Original text 1.
</span>

And your script used waitForKeyElements() like this:

waitForKeyElements ("span.myclass", wrapImportantWords);

function wrapImportantWords (jNode) {
    var newContent  = jNode.text ().replace (/Original/ig, "<span class='myclass2'>My GM</span>");
    jNode.html (newContent);
}

Yielding:

<span class="myclass" id="theirID1">
    <span class="myclass2">My GM</span> text 1.
</span>


Then, you could:

  1. Have wrapImportantWords return true -- which tells waitForKeyElements that the node was not found after all, so it keeps checking.
  2. Have that function also check to see if the appropriate span.myclass2 is (still) present.

Like so:

waitForKeyElements ("span.myclass", wrapImportantWords);

function wrapImportantWords (jNode) {
    if (jNode.has ("span.myclass2").length == 0) {
        var newContent  = jNode.text ().replace (/Original/ig, "<span class='myclass2'>My GM</span>");
        jNode.html (newContent);
    }
    return true;
}


Detecting vanished elements in general using the new MutationObserver:

In your specific case, this seems to be overkill. But, here's a method for general reference.

Notes:

  1. If you want to detect that a node is deleted, you have to set up the observer on its parent. Mutuation records don't seem to be available for when a node itself is deleted or completely rewritten.
    In this case, we want to know when span.myclass2s are deleted so we observe their parents (span.myclass).
  2. The Mutation Summary library allegedly makes this easier.

Here is a complete Firefox Greasemonkey script. You can test it against this page. (Note that the Chrome code is the same except for the usual changes due to the lack of @require.)

// ==UserScript==
// @name     _Node watcher 1
// @include  http://jsbin.com/exigal/*
// @include  http://YOUR_SERVER.COM/YOUR_PATH/*
// @require  http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @require  https://gist.github.com/raw/2625891/waitForKeyElements.js
// @grant    GM_addStyle
// ==/UserScript==
/*- The @grant directive is needed to work around a design change introduced
    in GM 1.0.   It restores the sandbox.
*/

waitForKeyElements ("span.myclass", wrapImportantWords);

function wrapImportantWords (jNode) {
    var newContent  = jNode.text ().replace (
        /Original/ig, "<span class='myclass2'>My GM</span>"
    );
    jNode.html (newContent);
}

/*--- Start of Mutation observer code...
*/
var targetNodes      = $("span.myclass");
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var myObserver       = new MutationObserver (mutationHandler);
var obsConfig        = {
    childList: true, characterData: true, attributes: true, subtree: true
};

//--- Add a target node to the observer. Can only add one node at a time.
targetNodes.each ( function () {
    myObserver.observe (this, obsConfig);
} );

function mutationHandler (mutationRecords) {

    mutationRecords.forEach ( function (mutation) {

        if (    mutation.type                == "childList"
            &&  typeof mutation.removedNodes == "object"
        ) {
            var remdNodes       = $(mutation.removedNodes);
            if (remdNodes.is ("span.myclass2") ) {
                console.log ("Desired node was deleted!   Restoring...");
                var targNode    = $(mutation.target);
                wrapImportantWords (targNode);
            }
        }
    } );
}
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
  • so, on your `waitFor` case, the return value trick is very nice. didn't realize it could do that. however, what I'm seeing is some cases where a `waitFor` setup on a parent (`myclass` in your example above) doesn't call `wrapImportantWords` when its inner text is rewritten. i've tried watching `myclass2` with `waitFor`, as well as the parent(s) of `myclass`. the failure to detect that change is intermittent in my case (the host site I'm working with is Facebook). can you think of a reason this might happen? i'll also try the mutation observer example next. thx! – mix Sep 27 '12 at 05:56
  • Is the inner text also deleting the `span.myclass2` (seems very doubtful) but, if it **is**: (1) Use Firebug or dev tools to capture the exact HTML of the offending `span.myclass` (the parent) and post that here. (2) Link to specific examples. (3) Try and come up with a recipe that would allow us to duplicate the glitch. ... If `span.myclass2` is NOT being deleted (likely), then change the checks upon `jNode`, inside of `wrapImportantWords`, to catch the changes you care about. – Brock Adams Sep 27 '12 at 06:21
  • yes, the `span.myclass2` vanishes. the code looks this way before deletion: ` others`. after deletion it looks like a version of this: `4 others`. i track `mycount` with `waitFor` but don't always get a notification of that change (sometimes I do, though). – mix Sep 27 '12 at 08:30
  • That just should not be possible, and *waitFor* passes every test case I can think to set up for it; I can't get it to fail this way. Link or post your full ***exact*** code (or post a fully self-contained code that demos the problem). If you haven't already: (1) check that no other scripts are running on the page, (2) shutdown FF/Chrome and use task manager to verify it's shut down, (3) uninstall and reinstall your script. – Brock Adams Sep 27 '12 at 08:53
  • Why are you turning on the sandbox? – ecmanaut Nov 24 '12 at 09:27
  • @ecmanaut, Turn on the sandbox to: (1) Stop interference with and from the page's javascript, (2) Allow `GM_` functions, (3) Guard against malicious escalation of privilege attacks, (4) A few other reasons I don't remember at the moment. But, really, (1) is enough -- it's inline with "DRY" and "atomic" code-design principles. – Brock Adams Nov 24 '12 at 10:05
  • Sorry, maybe I should have said I am one of the maintainers of Greasemonkey. :-) My curiosity stems from the fact that the Mozilla sandbox causes more problems than it solves – especially related to security (without the sandbox, you get no privileged API:s to attack) and DOM features breaking semi-mysteriously for people that work fine in their javascript consoles. It surprised me to see it advocated by a high-profile StackExchange member, especially one so active on the topic of user scripting. Moving to a world without that sandbox is actually much on behalf of better code/knowledge reuse. – ecmanaut Nov 25 '12 at 05:37
  • @ecmanaut, Thank you for your opinion. I do not agree, and this is not the place to discuss it. – Brock Adams Nov 25 '12 at 06:16