Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is `document.write` suitable for finding the element associated with the currently running script?

How can I find the script element associated with the currently running script? I'm only interested in scripts inside the body of the document, not in the head (or elsewhere).

Here is what I have so far, it seems to work alright. Are there any possible pitfalls I'm not thinking of?

function getCurrentScriptElement() {
    var placeholderId = 'placeholder-' + Math.random(), 
        script, placeholder;
    document.write('<br id="' + placeholderId + '">');
    placeholder = document.getElementById(placeholderId);
    script = placeholder.previousSibling;
    placeholder.parentNode.removeChild(placeholder);
    return script;
}

The use case I have in mind for this involves scripts that need to inject content at the same place in the document where the script tag occurs, so delayed loading will not be an issue. In other words I will only be calling this function in places where it's appropriate, it's not going to be exposed as part of an API or anything.

I'm also aware of the problem with document.write and XHTML, and am not worried about it in this case (so far).


My previous attempt looked something like this:

function getCurrentScriptElement() {
    var scripts = document.getElementsByTagName('script');
    return scripts[scripts.length - 1];
}

But, as we see in this question, this could easily fail if another script has been injected into the document.


I also found document.currentScript in this WHATWG spec. It doesn't seem to be widely supported at this point. There's an interesting shim for document.currentScript that takes another route... The script triggers an error, catches it, inspects the stack trace to find the URL of the script, and then finds the script element with the matching src attribute.

It's a clever solution, but it apparently won't work in IE, and it would break if several scripts use identical URLs.


Ideally, I would like to find a solution that gives the same result as the top code sample (no extra requirements), but doesn't rely on document.write. Is this possible? If not, is this code fairly safe assuming it's used correctly (only during page load, not XHTML)?


Edit: See this answer for another potential use case, and details on why a br element is used for the placeholder.

like image 860
Dagg Nabbit Avatar asked Jan 19 '13 03:01

Dagg Nabbit


1 Answers

OP has a good solution to a tricky problem. Yet, here are few minor suggestions to improve it.

The function should use document.currentScript when available. It works well regardless of situation. Available in FireFox +4, Chrome +29, and Opera +16.

With IE 6-10 the script.readyState can be used to emulate document.currentScript. It also appears to work well regardless of situation.

To avoid problems, the function should exit when document.readyState == "complete" or even at the earlier "interactive" stage depending on the code. document.currentScript appears to stop working when "complete". However, document.write stops at "interactive", i.e., the tag writing method of finding scripts will fail with a lazy loaded script.

The script async attribute should be checked before using document.write(). Scripts marked async are detached from the document and document.write() method will not work as expected.

OP's code writes a <p> tag, but a style tag appeared to work better with scripts in both the body and head ... and without side effects.

The random number tag ID doesn't seem to serve much purpose since the element is removed. Might be better to go with a GUID constant that's reused.

UPDATE:

I've updated my code sample below to illustrate a way to do this without writing tags. It appears to work fine with Chrome, FireFox, Opera, and IE 5-10. Returns the correct value regardless of injected scripts and script attributes (defer and async). The difficult part is what to do about IE +11? Microsoft removed script.readyState in IE11 without providing an alternative. One workaround is to use "MutationObserver" to look ahead for scripts. In IE these events events happen each time before a script is executed (as opposed to onload which happens afterward). And this works well except when there are externally loaded scripts with defer or async attributes set. Async scripts are especially difficult because they can execute anytime and not always in the same order (my observation).

Anyway, I thought I'd post this code with the hope it might provide a starting point and help others out.

References:

https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

http://msdn.microsoft.com/en-us/library/ie/dn265034(v=vs.85).aspx

CODE SNIPPET:

    var currentScript = (function() {

        // PRIVATE
        var observer, current, nodes;

        // IE+11 - untested with other browsers.
        if (!document.currentScript && typeof MutationObserver=='function') {
            observer = new MutationObserver(function(mutations) {
                if (document.readyState=='complete') observer.disconnect();
                mutations.forEach(function(mutation) {
                    nodes = mutation.addedNodes;
                    if (nodes.length && nodes[0].nodeName=='SCRIPT') {
                        current = nodes[0];
                    }
                });
            });
            observer.observe(window.document, {childList: true, subtree: true});
        }

        // PUBLIC
        return function() {

            if (document.readyState=='complete') return;

            // CHROME+29, FIREFOX+4, OPERA+16
            if (document.currentScript) return document.currentScript;

            var i, s=document.getElementsByTagName('SCRIPT');

            // MSIE+5-10
            if (s[0].readyState) 
                for(i=0; i<s.length; i++)
                    if (s[i].readyState=='interactive') return s[i];

            // IE+11
            if (current) return current;

            // BEST GUESS
            return s[s.length-1];

        }

    })()
like image 187
Roberto Avatar answered Sep 28 '22 06:09

Roberto