views:

945

answers:

6

I have a string representing a stand-alone (and valid XHTML 1.0 Strict) HTML document, something like

var html = "<?xml ... <!DOCTYPE ... <html><head><style>...</style></head>
                  <body><table>...</table></body></html>";

The body of this HTML document contains a table whose CSS-style is described in the head of the HTML document.

I also have a DOM-tree of another HTML document. How can I include into this DOM-tree the DOM-tree of the table with the correct style (as described in the HTML-string)?

I am especially interested in a jQuery-based solution.

EDIT: To be more concrete, an example of an HTML-string that I'm talking about is embedded into this XML-document.

+3  A: 

There's no sense in having two full DOM trees on the same page, so you'll want to extract out what you want and only use that.

Convert the string to a jQuery object and parse out what you need like this:

var html = "<html><head><style>...</style></head>
            <body><table>...</table></body></html>";

// Not sure if you are a trying to merge to DOMs together or just
// trying to place what you want on the page so, I'm going to go
// with the former as it may be more tricky.
var other_html = "<html><head><style>...</style></head>
                   <body><!-- some stuff --></body></html>";

var jq_html = $(html);
var jq_other_html = $(other_html);

var style = jq_html.find('head').find('style');
var table_and_stuff = jq_html.find('body').html();

jq_other_html.find('head').append(style).end().append(table_and_stuff);

That should probably do it. The CSS should automatically be recognized by the browser once it's inserted into the page.

NOTE:
For the new CSS style sheet to only add new styles and not override your current one(s), you must prepend the sheet to the head tag and not append it. So, the last line would need to be like this instead:

jq_other_html.find('head').prepend(style).end().append(table_and_stuff);
KyleFarris
Thanks, I've got something similar working. The problem now is that appending the CSS style to the head of the main document potentially overrides some styles that are being used by the elements of the main document. Is there a way to solve that problem?
Kaarel
Yeah, you can 'prepend' the CSS (same way as I described but with 'prepend(style)' instead of 'append(style)'. In this way, the current CSS you have will override the CSS in the newly attached CSS and only the new stuff the new CSS sheet provides will work. Remember, CSS, stands for 'Cascading Style Sheets'. Meaning, whatever is listed last and most specifically will be used.
KyleFarris
Well, if I prepend to the head then existing styles in the main document (which now come last) might override the style of the included bit. So adding anything to the head is not a good solution. Ideally, the style of the included DOM tree should be local to the DOM tree, and no other style should affect the included tree. Would that be possible?
Kaarel
+1  A: 

Instead of improving my other answer, I'd rather make a new one since I basically need to re-write it to work with how you want it and the old answer may help people for other reasons...

So, after extracting out the HTML-string from the XML you link to, you can go forth with it like this:

// First we'll extract out the parts of the HTML-string that we need.
var jq_html = $(html_from_xml);
var style = jq_html.find('head').find('style').html();
var style_string = style.toString();
var table_and_stuff = jq_html.find('body').html();

// If you want to place it into the existing page's DOM,
// we need to place it inside a new DIV with a known ID...
$('body').append('<div id="new_stuff"></div>');
$('#new_stuff').html(table_and_stuff);

// Now, we need to re-write the styles so that they only
// affect the content of the 'new_stuff' DIV:
styles_array = style_string.split('}');
var new_styles = '';
$.each(styles_array,function(i) { 
    if(i<(styles_array.length-1)) { 
        new_styles += "#new_stuff "+styles_array[i]+"}"; 
    }
})
$('head').append('<style type="text/css">'+new_styles+'</style>');

And that should really do it. The trick is that CSS will choose the most specific style for the case. So, if you have a <td> inside the "newstuff" div, it will get the style from the new stylesheet. If you have a <td> outside of that "newstuff" div, it will get the old style.

I hope this solves your problem.

KyleFarris
+13  A: 

I may be waaaay missing the point, but why not load the string into an IFRAME to render - this solves all the problems of having two separate DOM trees, and two separate sets of CSS rules, to me.

This code will do it:

$(function() {
        var $frame = $('<iframe style="width:200px; height:100px; border: 0px;">');
        $('body').html( $frame );
        setTimeout( function() {
                var doc = $frame[0].contentWindow.document;
                var $body = $('body',doc);
                $body.html(your_html);
        }, 1 );
});

(which I lifted from here: http://stackoverflow.com/questions/620881)

Then if you are concerned about the size of the IFRAME, you can set it with:

$frame[0].style.height = $frame[0].contentWindow.document.body.offsetHeight + 'px';
$frame[0].style.width = $frame[0].contentWindow.document.body.offsetWidth + 'px';
DanSingerman
Thanks, this looks quite clean. I'll need to test if it works out for me.
Kaarel
@HermanD An iframe seemed like the obvious solution... I presumed, seemingly incorrectly, that Kaarel had decided not to go with this. Oh well... Do you think there are any downfalls to placing the content into an iframe? Like increased difficulty of accessing the DOM of the iframe as opposed to it just being in the main DOM? Like, for instance, I don't think you can bind event listeners to elements inside an iframe with jQuery. Just a thought...
KyleFarris
@KyleFarris - I think as by definition there are two distinct DOMs, maintaining that rather than messing with it is the simplest solution. However, there are bound to be some drawbacks with the approach. Depends what the OP's intentions are really. The IFRAME DOM is certainly accessible via jQuery, but I haven't tried attaching event listeners. May try that later today if the day job doesn't get in the way.
DanSingerman
typo: offsetwidth -> offsetWidth
Kaarel
Some questions: (1) Why is the setTimeout needed? (2) Why do some variable names have a dollar sign (e.g. $frame)? (3) What does $frame[0] mean, i.e. why the [0] part?
Kaarel
@kaarel Ta - fixed!
DanSingerman
Well the code sample was borrowed from the other answer I linked to - but will try to answer your questions as best I can: (1) I think this must be some sort of Javascript evaluation order thing. Maybe for cross browser compatibility. The DOM element of the IFRAME seems not to be immediately available after instantiating it. (2) See http://stackoverflow.com/questions/205853/ - you can remove them if you like (3)jQuery always returns an array, even for single elements.
DanSingerman
@KyleFarris - had a try at binding events inside the IFRAME - worked just fine.
DanSingerman
@HermanD - that's good news. :-) Good work.
KyleFarris
+1  A: 

Insert it into an inline frame.

Using a question I asked earlier, I have a solution like this:

First, have an iframe, with some id

<iframe id="preview" src="/empty.html"></iframe>

Then style it:

iframe#preview
{
    margin: 30px 10px;
    width: 90%;
    height: 800px;
}

And here's a function to insert an html text into that frame (uses jquery)

function preview( html )
{
    var doc = $("#preview")[0].contentWindow.document
    var body = $('body',doc)
    body.html( html );    
}

I used this to successfully render the content of the html, including whatever embedded css it might include.

hasen j
Isn't that basically the same as my answer from an hour earlier?
DanSingerman
Your answer even references your own question the same as mine *scratches head*
DanSingerman
yeah, except it's a bit cleaner (I think) and more direct =P
hasen j
Thanks. Although ideally I wouldn't want to assign IDs to these iframes, nor set their size and margins. I have multiple HTML-strings that I want to include, and the places where they are included do not exist initially, they are also dynamically generated.
Kaarel
A: 

Here's some test code demonstrating how I'd do this. The code extracts the <style> element from the passed HTML and adds a custom ID to all the selectors, then injects the HTML into a <div> with that ID. That way all the CSS passed by the HTML will only apply to the injected table:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
<html xmlns="http://www.w3.org/1999/xhtml"&gt;
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Untitled Document</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"&gt;&lt;/script&gt;
<script type="text/javascript">
$(document).ready(function() {
 var html = '<?xml version="1.0" encoding="UTF-8"?>\n\
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"&gt;\n\
 <html xmlns="http://www.w3.org/1999/xhtml"&gt;\n\
 <head>\n\
 <title></title>\n\
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n\
 <style type="text/css">\n\
 div.dom { background-color: #ace; margin-bottom: 0.5em } table.drs { font-family: monospace; padding: 0.4em 0.4em 0.4em 0.4em; border: 1px solid black; margin-bottom: 2em; background-color: #eee; border-collapse: collapse }\n\
 td { vertical-align: top; padding: 0.3em 0.3em 0.3em 0.3em; border: 1px solid black }\n\
 td.op { vertical-align: middle; font-size: 110%; border: none }\n\
 </style>\n\
 </head>\n\
 <body><table class="drs"><tr><td><div class="dom">[]</div></td></tr></table></body>\n\
 </html>';

 var style = html.match(/<style([^>]*)>([^<]*)<\/style>/);
 var modifiedStyle = style[2].replace(/\s*(} |^|\n)\s*([^{]+)/g, " $1 #inserthere $2");

 $(document).find('head').prepend("<style" + style[1] + ">" + modifiedStyle + "</style");
 $("#inserthere").append($(html).children());
 });
</script>
</head>
<body>
<div id="inserthere">
</div>
</body>
</html>

Obviously you will have to modify this for your situation, but it's a starting point.

inxilpro
+1  A: 

Here is my code that does what you described. Two things to note

  1. For some reason find() did not work on the 'on-the-fly' dom that I got from jquery object, may be someone can find out what am I doing wrong there
  2. I have appended this new dom to 'body' element for illustration purposes. You could just as easily append it to your second dom


   var insertStr = "your string here";

   var newDom = $(insertStr); // getting [meta style table.drs]

   // I had to use the array as newDom.find would not work for me which I was expecting would work
   var styleText = $(newDom[1]).text();
   var tableElm = $(newDom[2]);
   $(newDom[2]).appendTo('body');

   var selectors = styleText.split(/{[\s\w\.\(\)':;"#%=/-]*}/);

   var styles = styleText.match(/{[\s\w\.\(\)':;"#%=/-]*}/g);



   for ( var i =0; i < styles.length; i++ ) {
       var style = styles[i];

       style = style.replace("{","");

   style = style.replace("}","");

   if ( selectors[i].indexOf("table.drs") > -1 ) { // identify top-level elm
       // apply to self
       tableElm.attr('style',style);

   } else {

       // apply to its children
       tableElm.find(selectors[i]).attr('style',style);

   }

}

Amit