Adding Nodes to the DOM with Style
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:
-
I left out attributes. If I set
border
and the author sheet setsborder-left
, it still wins forborder-left
. -
I was relying on children inheriting my rules. If I set
border
on a parent and the author sheet setsborder
on all elements, it still wins forborder
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:
-
Red is after green in the DOM, they are in the same stacking
context, and neither have
z-index
set? -
Red is after green in the DOM, they are in the same stacking context, and
they have the same
z-index
set? -
Red is after green in the DOM, they are in the same stacking context, and
green has a higher
z-index
than red? -
Red and green are in different stacking context's, red's context has a
higher
z-index
, and green has a higherz-index
than red? -
Red is after green in the DOM, they are in the same stacking context, and
that context is a XUL
stack
element?
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 div
s 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-index
s 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-index
s 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:
- https://developer.mozilla.org/en/XBL/XBL_1.0_Reference/Anonymous_Content#Binding_Stylesheets
- https://developer.mozilla.org/en/XBL/XBL_1.0_Reference/Elements#binding
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:
- Mike Kaply for taking an interest in my work and encouraging me to share it.
- Mitchell Newberry for hacking out some code for me and suggesting the name.
The source for mft3k is BSD licensed. Use it, make it better, and brag about it to me.
Comments
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.
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…
Colby Russell There were a bunch of bugs relating to
transparency in XUL panel
s 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 panel
s,
I'm talking about using XBL instead of observably adding stuff via (W3C) DOM
methods.
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!
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.
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.