Adding Nodes to the DOM with Style

posted jun 29, 2010 at 3:00pm on borderstylo

This blog post was originally written for Border Stylo. The best way to thank them for letting me republish it is to check out spire.io, kickass APIs for web and mobile apps.

Extension developers who want to overlay HTML on pages face two style-related challenges: preventing page styles from affecting the added HTML, and ensuring that the added HTML is visually over top of the rest of page. This blog post will show you how to use XBL to add nodes to the page with style, complete with a working demo.

Short Version

Play with this extension. It adds the row of seats from MST3K to your browser in a way that is interesting to browser extension developers.

The Cascade Problem

One of the first bugs I worked on in extension development was that the elements that extension was adding to a page would look strange of some pages and fine on others. What was happening was that CSS rules on those pages were being applied to our markup.

The first thing I researched while trying to fix this problem was how the cascade in CSS works—when an attribute for an element is specified twice, which version wins? It's a short read that I'd encourage everyone mucking about with CSS to skim at least once. The important lesson for me at the time was: user !important rules win.

"Great," I thought, "I'll just put all of our rules in a user sheet and mark them !important." And then I did (See the Stylesheet Service for how). Aside from being crazy, there are two reasons this didn't work:

  1. I left out attributes. If I set border and the author sheet sets border-left, it still wins for border-left.
  2. I was relying on children inheriting my rules. If I set border on a parent and the author sheet sets border on all elements, it still wins for border on the children of my element.

My next solution was to add rules to my user sheet for every element in my overlay with a list of every attribute set to -moz-initial !important except for the rules I wanted to change. I nicknamed this solution "screaming" at the top of your lungs all of the time." And it sort of worked. I had to pick ridiculous (or, as I said at the time, "improbable") ids for every element to avoid altering elements on the page, I had made it impossible to ever change the look of the extension, the error console was an avalanche of css errors, but the elements looked just the way I wanted them to on every page.

What I really needed was a way to tell Firefox "don't apply the page styles to this html." If you ever find yourself angrily tacking margin-left, margin-right, margin-top… to an inline style on an iframe, you probably need a way to do that, too. More on that later.

The Z-Index Problem

A red div and a green div are absolutely positioned with the same top, left, height, and width. Which color will be on top when:

Insert comic turntable squeak here.

Having "solved" the CSS problem, I moved on to the next style problem: how to make sure my element is over top of arbitrary page elements. In my experience, you can get 80% there by just setting z-index: 50 for the div you want to be over top of the page. Few elements have a z-index in the wild, and when they do developers seems to stick to single digits.

My favorite example of the other 20% is the dropdown menu on Yahoo! Finance. The z-index for these divs is set to about 2000. Why 2000? Who knows! But there's an easy fix: up your z-index from 50 to 2001. If anyone asks why you chose that number, make a joke about Stanley Kubrick to distract them and then run.

Obviously, 2000 is not the highest z-index in use on websites, so your qa lab is going to come back with some nasty bugs about ads for weight loss overlaying your UI. My brilliant plan on hearing this was to skip the intermediary escalation of z-indexs and jump to the highest z-index possible in firefox The problem with this is that, if I can look up the highest z-index, so can Joe Geocities Page. Having picked the highest z-index I could, I moved on to solving the problem of what to do with elements that are the same z-index as me.

Since my extension waited until the page loaded and then called document.body.appendChild, my element would be later in the DOM then those other nodes, so I'd beat them—yay! Unless the page added elements to the DOM after load, which since the whole "DHTML" fad in the 90s had become incredibly common.

If CSS wouldn't help us, it was time to escalate to Javascript. I considered continually moving my div to the bottom of the list of the body's children. I considered looping over all the other elements on the page and making their z-indexs smaller than mine. In short, I considered madness.

What I really needed was a way to tell the page "this HTML should go on top of all of the other markup." If you have a tab opened to the w3c page for "stacking context" to follow a wild hunch, you probably need a way to do this as well.

A Wacky Problem Calls for a Wacky Solution

Mystery Firefox Theater 3000 is a trivial example of a solution to the cascade and z-index problems.

How does this help with the cascade?

The solution to our first problem is to put our HTML inside of a XBL binding with the attribute inheritstyle set to false. There are two places on MDC that mention inheritstyle. Don't blink or you'll miss them:

mft3k's content.js waits for pages to load and applies a binding to the html node of each page (the reason I chose the HTML node will be explained when I come back to the z-index problem). binding.xml#seats has inheritstyle="false" and adds some HTML around the body of the page.

The rules specified in stylesheet.css are not exhaustive lists of attributes set to !important, there is no need for inline styles, and they can make use of the normal cascade. The only gotcha I've encountered with inheritstyle is that it doesn't seem to work for font-related styles, but adding a nice * {color: -moz-initial !important, … other font-related styles … } to the top of my binding's stylesheet seems to do the trick. Limiting the pain to some font-bleeding problems is a huge win.

How does this help with the z-index problem?

Here's where things get down-right devious. As noted above, the body is now a child of some of my binding's DOM nodes. I give the parent of the page body and my div a display of -moz-stack, which causes it to act like a XUL stack.

Since my seats div comes after the body as a child of the stack, it will always be displayed on top of the body. The stylesheet has some extra rules to make sure the body's height and width are what you'd expect, and we're done.

The major gotcha here is that plugins like Flash wildly ignore the rules about who's on top on Windows and Linux. I'll have to leave this one as an exercise to the reader, and please turn in your homework when you're done :)

Thanks and Conclusion

I'd like to say thanks to:

The source for mft3k is BSD licensed. Use it, make it better, and brag about it to me.

Comments

Jorge on july 2010

I’m curious if this approach will continue to work when remote XUL is eliminated (https://bugzilla.mozilla.org/show_bug.cgi?id=546857), assuming this approach requires mixed XUL and HTML content in a web page.

Colby Russell on july 2010

I started asking myself literally last week why extension authors (especially the big players, like the Firebug guys) aren't using XBL instead of injecting elements into the page and possibly causing observer effects.

I really don't know understand why you call it a wacky solution. It has the shadow tree for not mucking up the DOM and was designed for encapsulation.

Ryan on july 2010

Jorge: Aside from making a html:div act like a xul:stack with -moz-stack, XUL is mercifully absent from my hack. I will be sure to look into that bug though, thanks!

Colby Russell: It is oddly uncommon, but I'm certainly not the first developer to stumble across that corner of MDC. For example, if think the guys at Flashblock make excellent use of XBL + observers. I hope that releasing a cheesy demo like mft3k will help spread the word.

Regarding the wackiness, you may be right. It's only bizarre until everyone starts doing it :)

Colby Russell on july 2010

Ryan: Flashblock actually goes further with encapsulation by making it such that none of the methods are available to content. I don't know if I'd call it a good use of XBL, though, because the way it's done is by not creating any methods—all of the "methods" are actually functions defined in a closure of the constructor, and they work because everything happens either in the constructor or by being invoked by an event listener (which has access because it's also set in the constructor).

That's by no means a slam on Ted, Ted, Jesse, or any of the others involved. I say that I wouldn't call it a good use of XBL just because the approach isn't very XBLly, since none of methods, fields, or handlers are actually specified in XBL. But that was the only way to achieve that with XBL+JS (at least at the time it was originally written, I think), and it was just a deficiency of XBL. The HTML5 video controls are actually specified in XBL, too, without exposing the implementation to content, but I don't know how it achieves that, since I've only ever accidentally stumbled upon it while browsing without the intent of figuring out how it works. I believe nsISecurityCheckedComponent is involved somehow, for what it's worth.

Mook on july 2010

Note that the planned removal of remote XUL also plans to remove remote XBL, so it might indeed break your solution.

I wonder if something involving a <panel> (similar to the autoscroll marker or the Test Pilot status messages) with noautohide=true will work. It used to not play well with transparency, but that might have been fixed now…

Mike Ratcliffe on july 2010

Colby Russell There were a bunch of bugs relating to transparency in XUL panels and these really made it difficult to implement a XUL version of the inspector.

All of the things in this article are more than familiar to me — I am planning on migrating the Firebug inspector to a transparent XUL panel and SVG / canvas.

Great article.

Colby Russell on aug 2010

Mike Ratcliffe: I wasn't talking about XUL panels, I'm talking about using XBL instead of observably adding stuff via (W3C) DOM methods.

Ryan on sep 2010

I increased MFT3K's maxVersion today to 4.* and tried in out on Minefield 4.0b5pre. It ran without changes! Luckily, the removal remote XUL have not nerfed this approach to adding dom nodes!

Thanks to Jorge for bringing the remote XUL change to my attention, and Mook for clarifying that remote XBL was also on the chopping block!

Ryan on oct 2010

Just one last update for posterity:

Some early versions of 4 were missing the remote XUL nerf, which is why mf3tk worked the day I tried it. Mike Kaply was nice enough to let me know the day they actually took out remote XUL, and sure enough my demo extension was hosed.

Luckily, the flash block dev was nice enough to let my coworker Nick know of a workaround: https://forums.addons.mozilla.org/viewtopic.php?f=12&t=1437&p=4849#p4849

See the head of mf3tk for an example of XBL that really, really works in FF4.