human
human

Reputation: 736

How can I get back to the original DOM after being affected by javascript

Imagine I have a loaded HTML page which has been already affected by javascript adding/deleting dynamic elements or new classes/attributes/id to elements while initializing(e.g: original source code [html] tag has no classes, after javascript loads [html] tag has class="no-responsive full-with"). Imagine after that I add/amend some id values manually (through my app). And imagine I need to be able to save in database the original source code (without any amends) but with the id attributes I added manually.

Basically I need to add a given id attribute to an element within the source code of an HTML, loaded through PHP.

Do you guys have any idea of how to do such a thing?

Upvotes: 0

Views: 1085

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074276

There's no simple solution here. The exact nature of the complex solution will be determined by your full set of requirements.

Updated Concept

You've said that in addition to changing things, you'll also be adding elements and removing them. So you can't relate the changed elements to the originals purely structurally (e.g., by child index), since those may change.

So here's how I'd probably approach it:

Immediately after the page is loaded, before any modifications are made, give every element in the a unique identifier. This is really easy with jQuery (and not particularly hard without it):

var uniqueId = 0;
$("*").attr("data-uid", function() {
    return ++uniqueId;
});

Now every element on the page has a unique identifier. Next, copy the DOM and get a jQuery wrapper for it:

var clone = $("html").clone();

Now you have a reliable way to relate elements in the DOM with their original versions (our clones), via the unique IDs. Allow the user to make changes.

When you're ready to find out what changes were made, you do this:

// Look for changes
clone.find("*").addBack().each(function() {
    // Get this clone's unique identifier
    var uid = $(this).attr("data-uid");

    // Get the real element corresponding to it, if it's
    // still there
    var elm = $("[data-uid=" + uid + "]")[0];

    // Look for changes
    if (!elm) {
        // This element was removed
    }
    else {
        if (elm.id !== this.id) {
            // This element's id changed
        }
        if (elm.className !== this.className) {
            // This element's className changed
        }
        // ...and so on...
    }
});

That will tell you about removed and changed elements. If you also want to find added elements, just do this:

var added = $(":not([data-uid])");

...since they won't have the attribute.

You can use the information in clone to reconstruct the original DOM's string:

clone.find("[data-uid]").addBack().removeAttr("data-uid");
var stringToSend = clone[0].outerHTML;

(outerHTML is supported by any vaguely modern browser, the latest to add it was Firefox in v11.)

...and of course the information above to record changes.

Live proof of concept

HTML:

<p class="content">Some content</p>
<p class="content">Some further content</p>
<p>Final content</p>
<input type="button" id="makeChange" value="Make Change">
<input type="button" id="seeResults" value="See Results">

JavaScript:

// Probably unnecessary, but I wanted a scoping
// function anyway, so we'll give the parser time
// to completely finish up.
setTimeout(function() {
    // Assign unique identifer to every element
    var uniqueId = 0;
    $("*").attr("data-uid", function() {
        return ++uniqueId;
    });

    // Clone the whole thing, get a jQuery object for it
    var clone = $("html").clone();

    // Allow changes
    $("#makeChange").click(function() {
        this.disabled = true;
        $("p:eq(1)").attr("id", "p1");
        $("p:eq(2)").addClass("foo");
        alert("Change made, set an id on one element and added a class to another");
    });

    // See results
    $("#seeResults").click(function() {
        this.disabled = true;

        // Look for changes
        clone.find("*").addBack().each(function() {
            // Get this clone's unique identifier
            var uid = $(this).attr("data-uid");

            // Get the real element corresponding to it, if it's
            // still there
            var elm = $("[data-uid=" + uid + "]")[0];

            // Look for changes
            if (!elm) {
                display("Element with uid " + uid + ": Was removed");
            }
            else {
                if (elm.id !== this.id) {
                    display("Element with uid " + uid + ": <code>id</code> changed, now '" + elm.id + "', was '" + this.id + "'");
                }
                if (elm.className !== this.className) {
                    display("Element with uid " + uid + ": <code>className</code> changed, now '" + elm.className + "', was '" + this.className + "'");
                }
            }
        });
    });

    function display(msg) {
        $("<p>").html(String(msg)).appendTo(document.body);
    }
}, 0);

Earlier Answer

Assuming the server gives you the same text for the page every time it's asked, you can get the unaltered text client-side via ajax. That leaves us with the question of how to apply the id attributes to it.

If you need the original contents but not necessarily identical source (e.g., it's okay if tag names change case [div might become DIV], or attributes gain/lose quotes around them), you could use the source from the server (retrieved via ajax) to populate a document fragment, and apply the id values to the fragment at the same time you apply them to the main document. Then send the source of the fragment to the server.

Populating a fragment with the full HTML from your server is not quite as easy as it should be. Assuming html doesn't have any classes or anything on it, then:

var frag, html, prefix, suffix;
frag = document.createDocumentFragment();
html = document.createElement("html");
frag.appendChild(html);
prefix = stringFromServer..match(/(^.*<html[^>]*>)/);
prefix = prefix ? prefix[1] : "<!doctype html><html>";
suffix = stringFromServer.match(/(<\/html>\s*$)/);
suffix = suffix ? suffix[1] : "</html>";
html.innerHTML = stringFromServer.replace(/^.*<html[^>]*>/, '').replace(/<\/html>\s*$/, '');

There, we take the server's string, grab the outermost HTML parts (or use defaults), and then assign the inner HTML to an html element inside a fragment (although the more I think about it, the less I see the need for a fragment at all — you can probably just drop the fragment part). (Side Note: The part of the regular expressions above that identifies the start tag for the html element, <html[^>]*>, is one of those "good enough" things. It isn't perfect, and in particular will fail if you have a > inside a quoted attribute value, like this: <html data-foo="I have a > in me">, which is perfectly valid. Working around that requires much harder parsing, so I've assumed above that you don't do it, as it's fairly unusual.)

Then you can find elements within it via html.querySelector and html.querySelectorAll in order to apply your id attributes to them. Forming the relevant selectors will be great fun, probably a lot of positional stuff.

When you're done, getting back the HTML to send to the server looks like this:

var stringToSend = prefix + html.innerHTML + suffix;

Upvotes: 3

Related Questions