Friday, April 29, 2005

IE bold text + opacity problem

You know how IE's alpha opacity filter ruins boldface text?

<div style="width:100%;
     filter:alpha(opacity=90);
     font-weight:bold;">
  Hello world!
</div>

If you're using IE, you can see the problem in action here:

Hello world!

I discovered this while tweaking the opacity filter function I wrote about earlier.

After much fruitless Googling and consulting with webdev experts, I couldn't find a fix. And like most solutions, I had to stumble upon it at the eleventh hour: adding a background color or image to the container element will fix the IE opacity rendering problem.

<div style="width:100%;
     filter:alpha(opacity=90);
     font-weight:bold;
     background-color:#fff">
  Hello world!
</div>

Here it is again, with the fix:

Hello world!

Hopefully the search engines will find this page and no one will have to suffer as I have ever again.

UPDATE: SZOJ reader Andre points out that this bug probably surfaced when MS introduced ClearType rendering, which probably explains why the bug goes away when "font smoothing" is disabled.

Friday, April 22, 2005

Ugliest line trunc hack EVER

Warning: this hack is dumb and should not be used by anyone.

The challenge: given some text of abitrary length, figure out where to best truncate the text so that only the first two lines are displayed, regardless of the width of the containing element.

By the way, the element is inline, you have no control over its dimensions, and the spot where you truncate the text must end in an ellipsis (…) Don't ask. Q: can't we just cut it at 20 characters and call it a day? A: no, it must fully fill the two lines, do as we say or we will be cranky.

Given that the fonts used were not fixed-width, I threw out the idea of counting character widths. However, I knew that the line-height CSS rule for this element was 14 pixels, and using offsetHeight I could determine if the height of the containing element, in this case a SPAN. (Word to the wise: offsetHeight is what you want. In the absence of a specific rule, the computed CSS height property for an element seems to be either blank, undefined or "auto", none of which are particularly helpful.)

Armed with this knowledge, I set out to write the ugliest hack I could manage:

var elm = document.getElementById("DivOne");
while (parseInt(elm.offsetHeight) > 28){
    var txt = elm.innerHTML;
    txt = txt.substring(0, txt.length-2) + "&#8230;";
    elm.innerHTML = txt;
}

In a nutshell: take the text, shave off the last character, append an ellipsis HTML entity, then shove the text back into the element and measure the height. Repeat until the SPAN is finally 28px high (twice the line height == two visible lines).

Ugh. I'm lucky that the strings I was working with were no more than 30 characters long, and only 3-5 strings had to be dealt with in any given page. Excuse me while I go take a shower.

Thursday, April 21, 2005

DOM 0 with XHTML caveat

Here's an interesting gotcha, unearthed from Bobby van der Sluis' popular article on JavaScript good practices that's making the rounds: when you serve an XHTML document as application/xhtml+xml, the good old DOM Level 0 collections like document.images and document.forms go bye-bye, at least in Mozilla-based browsers.

I'm okay with this, but I love me some backwards-compatible DOM 0, especially when I know exactly which element I need to access via script. Luckily, it's perfectly okay to serve XHTML 1.0 as "HTML compatible" text/html. It's when you start serving XHTML 1.1 as text/html that people start yelling and pointing.

Here's the Bugzilla discussion of this behavior.

Monday, April 18, 2005

Odd and ends 4/18/2005

If you sent me a message in the past month, my apologies: I forgot to turn on Gmail forwarding and just discovered a backlog of SZOJ email. I'll catch up in a few.

Speaking of Gmail, here's some reverse JS Zen: Gmail now works in nearly all browsers, including some legacy ones. Gmail falls back on a basic HTML version if it detects that the browser can't support the more advanced features. So while you might not have all the real-time widgetry, you'll never be locked out of your account when you're stuck with Netscape 4.

I've heard lots of comments from developers in years past about how users have to expect a greatly degraded experience if they're not willing or able to upgrade at the client end. Unfortunately, "degraded" often means "not even minimally supported." The Gmail developers weren't willing to settle for that, and neither should the rest of us. Design your web app for the web first. (Now, if only we can get more Flash developers to buy in to this.)

On a different tangent: earlier this week I wrote a post about what I thought about the whole "controversy" over the use of "Ajax" as a suitable term to describe DOM + JS + behind-the-scenes HTTP. I ended up scrapping it because I want this weblog to focus on practical stuff. Leave it to Matt to sum up my thoughts, nearly to the letter. So I'm going allow myself a (hopefully infrequent) opinionated post: hello, our technology just became interesting again to the world at large, and y'all want to complain about buzzwords? Who the heck cares what they end up calling it?

I hereby call a moratorium on the snarkiness, name-calling and faux outrage over something we failed to do ourselves: make DHTML popular.

Friday, April 15, 2005

Shorter JS Iron Chef!

The challenge: write a JS function that takes a given hex color and returns a suitably-contrasted greyscale shade, in as few lines of JS as possible.

My first pass went like this:

function contrast(c) {
    var av = (parseInt(c.substring(1,3),16) +
              parseInt(c.substring(3,5),16) +
              parseInt(c.substring(5,7),16)) / 3;
    av = (av >= 100)? av-100:av+100;
    return "rgb(" + av +"," + av + "," + av + ")";
}

A quick overview of what's happening here. c is the 6-character hex color (sans the "#"). The first JS statement uses the substring function to break the hex color string into its three 2-character hex pairs, which are then passed to parseInt to convert them to decimal values. Then we find the average by adding them and dividing by 3.

So, FF0000 (pure red) becomes FF+00+00 becomes 255+0+0 becomes 255/3 becomes 85.

The function then looks at this average and, using 100 as its "bias" point, either adds or subtracts 100 (an arbitrary value that produced the best results for our needs). The last line returns a greyscale RGB value (perfectly valid in CSS! No need to convert back to hex!).

Now the fun: I sent this off to another webdev for review and challenged him to make it even shorter. He came back with this:

function f(c,n) {return parseInt(c.substring(n,n+2),16);}

function contrast(c) {
    var av=(f(c,1)+f(c,2)+f(c,3))/3;
    av+=100*(av>=100)?-1:1;
    return "rgb("+av+","+av+","+av+")";
}

Nice, but I pointed out that av wasn't being calculated correctly without an additional set of parenthesis. Plus, we could shave off a few characters by replacing substring with the shorter substr to the same effect. I countered with this:

function f(c,n) {return parseInt(c.substr(n,2),16);}

function x(c) {
    var av=(f(c,1)+f(c,3)+f(c,5))/3;
    av+=(av>=100)?-100:100;
    return "rgb("+av+","+av+","+av+")";
}

Of course, I hadn't thought to remove the whitespace yet :) His riposte:

function f(c,n){return parseInt(c.substr(n,2),16);}
function x(c){var a=(f(c,1)+f(c,3)+f(c,5))/3;c=100;
  a+=(a>c)?-c:c;c=",";return "rgb("+a+c+a+c+a+")";}

Bested but not broken by his ingenious re-use of local variables, I nonetheless proclaimed victory by deleting the requirement for such a JS function from the spec, reducing the necessary code to:


Don't ever let it be said I don't think outside the box!

Tuesday, April 12, 2005

Memory leaks explained

If you're new to building complex web apps that use tons of JavaScript-powered DOM manipulation, you are hereby required to read Joel Webber's excellent article DHTML Leaks Like A Sieve, which eloquently summarizes the challenge of memory leakage in JS-heavy applications.

You may in fact wish to bookmark Joel's weblog and follow along as he unearths neato facts about Google Maps and other modern web apps.

Friday, April 08, 2005

IE6 opacity filter caveat

This one was a real pain. I was lucky to guess the solution.

I was working on a very small function to set alpha transparency on elements. This is what I came up with:


var ie = (document.all) ? 1 : 0;
var p = (ie) ? "filter" : "MozOpacity";

/* n is the element node
   v is the opacity value, from 0 to 100. */

function op(n,v){
    v = (ie) ? "alpha(opacity="+v+")" : v/100;
    n.style[p] = v;
}

(Yes, I know I left Opera and Safari/KHTML out of the equation.)

This function worked great in Firefox but nothing was happening in IE6. I tried a bunch of different tweaks, none of which worked. Adding an alert(n.style.filter); at the tail end of the function returned "alpha(opacity=50)" indicating that the value was being set correctly for IE6. But the element still rendered at 100% opacity.

Finally, I threw the function out altogether and applied the filter directly to the DIV element via CSS:

<div id="myDiv" style="filter:alpha(opacity=50);">
<img src="MyImage.gif" alt="Some Image Here" 
  width="50" height="50" />
<br />
This should be transparent.
</div>

But it still didn't work! IE6 stubbornly ignored the opacity rule. Even more confusing was that moving the opacity rule to the image alone did work. But only on the image, of course.

<div id="myDiv">
<img style="filter:alpha(opacity=50);" src="MyImage.gif" 
  alt="Some Image Here" width="50" height="50" />
<br />
This should be transparent.
</div>

I really needed the opacity applied to both the text and image. I was about to give up when it occurred to me that maybe the image width and height dimensions has something to do with it. By default, a block element takes on the width of its containing element and height of its content, but maybe IE6 was ignoring this when applying the alpha filter. So I tried this:

<div id="myDiv" 
  style="filter:alpha(opacity=50);
     width:100%; height:100%;">
<img src="MyImage.gif" alt="Some Image Here" 
  width="50" height="50" />
<br />
This should be transparent.
</div>

Yes! Opacity was now being applied to both the image and the text. I removed opacity rule from the DIV and was then able to correctly set opacity via the JavaScript function.

I don't have the luxury of testing this across the different flavors of IE, but it appears that alpha opacity in IE6 requires certain elements to have a specified width and height. What's funny is that when I tried Googling for answers to my problem, I found a ton of working examples that used opacity in IE6 successfully. Looking back now, all of those examples specified a CSS width and height on the element, but didn't make any specific mention of it as a requirement for IE. Removing the width and height caused those examples to fail, too.

UPDATE: Dean Edwards points us to the MSDN documentation for this little-known behavior. IE filters only apply to elements with "layout."

Wednesday, April 06, 2005

Variable scoping gotchas

Phil points to this great bit by Scott Issacs about JavaScript variable scoping. I've seen hardcore programmers as well as JS newbies make the same simple mistake:

function Foo() {
  bar = 1;
}

You might assume bar is private to Foo, but you'd be mistaken. Variables declared without the var operator are global by default. The correct syntax is:

function Foo() {
  var bar = 1;
}

Check out Scott's weblog for more scripty goodness.