Style anything in IE

A lot of people have been writing about this recently, and I can't take credit for discovering it. I first saw it here and then again here, but I'll recap it briefly on this blog for anyone who is too lazy to follow those links.

Under normal circumstances, IE won't allow you style tags that it doesn't recognize. But there is an interesting quirk in IE that will allow you to work around this and style unknown elements. All you have to do is create an element whose tag name is that which you desire to style, like so:

document.createElement('myNewTag');
Note that there is no need to actually add the newly created element to the DOM, or even store it in a variable. Just the process of "creating" the element is enough to "wake IE up" to the fact that such a tag exists.

With the Javascript in place, you could then do:


<style type = "text/css">
myNewTag
{
   border: 5px solid #f00;
   background: #00f;
   font-weight: bold;
}
</style>
<myNewTag>Brand new tag that has never existed before!</myNewTag>

Now IE will happily style all <myNewTag> elements with a red border, a blue background and bold text.

This goes for any tag that IE would not normally recognize, but it may prove especially useful in handling HTML5 down the road.

So there ya go... another IE quirk "to the rescue."

Accessing the Native DOMElement's prototype in Safari 2

UPDATE:

It turns out, Safari has an interesting quirk. Simply executing this Javascript:

document.getElementsByTagName('html')[0];
is enough to make the "[[DOMElement.prototype]]" property come into existence. This appears to be true regardless of how early in the page load cycle you execute the Javascript, so perhaps the whole polling script I suggest below is unnecessary after all.

For the longest time, I thought it was just completely impossible to access the prototype of the native DOMElement (or HTMLElement in most other browsers) in Safari 2.

As an alternative, I'd see people extend the Object object's prototype in order to add functionality to DOM elements (heck, I've done this myself -- shame on me). This is problematic for a couple of reasons:

  1. Programmers expect var myObj = {} to be an empty object (i.e. it should have no properties). Extending Object's prototype breaks this.
  2. Iterating an object's properties via for .. in now requires the use of .hasOwnProperty(..) to ensure you're not accessing properties you don't intend to access.

I searched high and low, and I experimented (numerous times) trying to find some way of accessing the native DOMElement in Safari 2, but always to no avail... until now.

It's worth noting that this is not an issue in Safari 3. Extending DOM elements in that browser is easy:
HTMLElement.prototype.foo = function()
{
   alert('foo');
};
// assume elem is a reference to some valid DOM element
elem.foo();       // alerts "foo"

At long last, a solution for Safari 2

Credit totally goes to my co-worker, Derrick Quan, as he is the one who discovered the initial solution.

It turns out there there is a property on the window object in Safari 2 which is identified by the string "[[DOMElement.prototype]]" (case sensitive and double brackets on both sides required).

Now you can't access this directly because [[DOMElement.prototype]] is not a valid variable name. It doesn't have to be though, since it lives as a property of some other object (the window object in this case).

This means that we can access "[[DOMElement.prototype]]" by treating it like what it is (a property on the window object).

var DOMElemProto = window['[[DOMElement.prototype]]'];
DOMElemProto.foo = function()
{
   alert('foo');
};
// assume elem is a reference to a valid DOM element
elem.foo();          // alerts "foo"

There is "gotcha" however (maybe not -- see the UPDATE note at the beginning of this post).

It seems that the [[DOMElement.prototype]] property is not available until the DOM is fully loaded, which means if you want to be confident in accessing the property, you'll need to do it, either in a window "load" listener, or by using a DOMContentLoaded-type script (see Hedger Wang's post: An alternative for DOMContentLoaded on Internet Explorer and/or Stuart Langridge's: DOMContentLoaded for IE, Safari, everything, without document.write).

Alternatively, you could poll for the existence of the [[DOMElement.prototype]] property, and take action once it finally "comes alive."

function getDOMElemProto(_retries)
{
   this.retries = this.retries || _retries;
   if ((this.count = (this.count || 0) + 1) > _retries)
   {
      // abort
      return;
   }

   var DOMElemProto = window['[[DOMElement.prototype]]'];
   if (typeof DOMElemProto === 'undefined')
   {
      setTimeout(getDOMElemProto, 100);
   }
   else
   {
      extendDOMElements(DOMElemProto);
   }
}

function extendDOMElements(_proto)
{
   // Extend the DOMElement.prototype as necessary here
   _proto.foo = function()
   {
      alert('foo');
   };

   _proto.bar = function()
   {
      alert('bar');
   }
}
getDOMProto(50);

After a few retries, Safari2 should be able to access [[DOMElement.prototype]], while other browsers will give up after 50 tries.

Now I haven't done extensive testing on this, but initial tests seem to indicate that this works quite well. And it doesn't resort to extending the Object object's prototype at all, so users of this script should not have to worry about object literals containing unexpected properties.

I think this is pretty cool.

Comments, questions, suggestions, complaints... all welcome.

setCapture with Gimme

In my last blog post I talked about how to re-route events in Javascript. It comes in handy from time to time, but some people prefer an alternate paradigm -- one that is probably more familiar to Win32 programmers.

It's called setCapture. It's proprietary, and it's native to only the IE browser (as far as I know).

What's .setCapture() good for?

Put simply, .setCapture() redirects all mouse events to a specified DOM element until a call to .releaseCapture() is made. Why would you want this? I can think of a couple scenarios.

  1. Drag n' Drop: you wire up mousedown to some element to start the drag operation, but wiring up mousemove and mouseup on that same element could be problematic -- the user may mouse too fast and your code might not be able to "keep up." Using .setCapture() on said element however, will redirect all mouse events to the appropriate element so that the mousemove and mouseup handlers are guaranteed to execute.
  2. Dropdown Menus: you want the next mousedown to dismiss a dropdown menu. A first crack at this might be to wire up mousedown to the <body> element, but if some other element on the page is listening for mousedown and is stopping event propagation, the mousedown may never make it to the <body> element -- .setCapture() will solve this problem.

These aren't the only applications of .setCapture() of course, but they're two of the most common and they're a good illustration of how .setCapture() can be useful.

Gimme's version of .setCapture(..)

Even though Mozilla/Firefox and other W3C browsers don't natively support .setCapture(), Gimme solves this problem by simulating the capability for browsers that don't natively support it. Using Gimme's version is a snap.

// just a shortcut for document.getElementById(..)
var dragElem = Gimme.id('dragElem');

// #dragElem will capture all mouse events until
// a call to Gimme.Events.releaseMouse() is made
Gimme.Events.captureMouse(dragElem);

A simple call to Gimme.Events.captureMouse(myElem) will do the trick (where myElem is the DOM element that should capture all mouse events).

This example, of course, doesn't accomplish anything very useful, but here's an example that's a bit more practical.

This snippet gives drag/drop capabilities to all elements marked with the class "draggable" (it looks long but that's just comments and a big font).

g('.draggable').iterate(makeDraggable);
function makeDraggable(idx)
{
   // variables to keep track of the drag element's start position
   // and the initial mousedown position
   var elemStartPos, mouseStartPos;

   // wire up mousedown to start the drag operation
   // and stop click events from propagating (just in case)
   this
   .addEvent('mousedown', mouseDown)
   .addEvent('click', stopEvent);

   function mouseDown(e)
   {
      // instruct all mouse events to be redirected to the current dragElem
      Gimme.Events.captureMouse(this);

      // prevent the mousedown event from propagating
      e.stopPropagation();

      // create a gimme object from the element being moused down on
      var gObj = g(this);

      // keep a ref to the dragElem's start position
      // and to the initial mousedown position
      elemStartPos = gObj.getPagePosition();
      mouseStartPos = Gimme.Screen.getMousePosition(e);

      // now, hook the mousemove and mouseup events
      gObj
      .addEvent('mousemove', mouseMove)
      .addEvent('mouseup', mouseUp);
   }

   function mouseMove(e)
   {
      // prevent the mousemove event from propagating
      e.stopPropagation();

      // get the current mouse position
      var mousePos = Gimme.Screen.getMousePosition(e);

      // position the dragElem (element's start position + mouse delta)
      g(this)
      .setStyle('top', elemStartPos.y + (mousePos.y - mouseStartPos.y) + 'px')
      .setStyle('left', elemStartPos.x + (mousePos.x - mouseStartPos.x) + 'px');
   }

   function mouseUp(e)
   {
      // instruct Gimme to stop capturing mouse events
      Gimme.Events.releaseMouse();

      // prevent the mouseup event from propagating
      e.stopPropagation();

      // remove the mousemove and mouseup handlers
      g(this)
      .removeEvent('mousemove', mouseMove)
      .removeEvent('mouseup', mouseUp);
   }

   function stopEvent(e)
   {
      e.stopPropagation();
      e.preventDefault();
   }
}

Before I get into an explanation of the code, the following note is significant:

The current version of Gimme (Belisar) doesn't offer Gimme.Events.captureMouse(..). This capability is new to Gimme (Caspian) which is currently in a pre-release state. You can access Caspian now by visiting the Source Code Section on the Gimme Codeplex site, or you can wait for the official release of Caspian, which should be sometime in late January.

Explanation

If you understand Gimme, the code here is really pretty straight-forward.

A mousedown event starts the drag operation by recording the mouse down position and the element's start position. Then, the mouseMove handler ensures that the element moves along with the mouse delta (delta from the mouse start position). Finally, the mouseUp handler ends the drag operation by removing event handlers that are no longer needed and releasing capture.

We prevent events from propagating (bubbling) in all cases, so that we don't get other elements interfering during a drag operation. And we use Gimme's Gimme.Events.captureMouse(..) capabilities to ensure the the mousemove and mouseup events are directed to the appropriate element. Otherwise we risk some other element "stealing" the mousemove or mouseup away from the intended element.

What does this refer to exactly?

Good question; it's the one thing in the Gimme drag/drop code that I can see tripping people up. The this keyword is somewhat polymorphic in Gimme, so what does it refer to exactly?

The answer is that is depends on context. Most of the time in Gimme, you'll be using this inside of an event handler function. In that case, this refers to the DOM element to which the event was wired up. For example:

g('#myElem').addEvent('click', function(e)
{
   // alerts "myElem"
   // since 'this' refers to #myElem in the current context
   alert(this.id);
});

In some cases though, you may want to use the Gimme .iterate(..) function. In that case, this refers to the current Gimme Object being iterated over, not a DOM element, but it's possible to combine both concepts. For instance:

g('#container > .funny').iterate(function(idx)
{
   // here, 'this' refers to the current Gimme Object,
   // which is why .addEvent(..) is okay here
   this.addEvent('click', function(e)
   {
      // here, 'this' refers to the DOM element receiving the click event,
      // so we can access native properties, like .parentNode
      this.parentNode.removeChild(this);
   });
});

In case you hadn't figured it out, the above snippet makes it so that all elements with the class "funny" that are direct children of #container will be removed from the DOM when clicked -- pretty trivial with Gimme.

But this is all secondary to the discussion at hand: capturing mouse events, which, thanks to Gimme, is also pretty trivial!

If you're new to Gimme, check out the Gimme Promo Page and the Gimme Codeplex Page.

You can also download a debug version of Caspian here if you just want to monkey around with it.

Note once again, that an official (and compressed) version of Caspian will be available sometime in late January.

Comments welcome.

Re-routing Events in Javascript

Sometimes, it is desirable to re-route an event in Javascript, from its original target to some other target. Suppose for example, you have two different elements that appear to be layered, one of top of the other, but they only appear this way because of CSS. In terms of the DOM, the elements would be siblings, as in:

<div id = "elem1">#elem1</div>
<div id = "elem2">#elem2</div>

With a little CSS, that HTML might look something like this:

#elem1
#elem2

It should be obvious, but I'll draw attention to it again because it's important for this discussion: #elem2 is not actually contained within #elem1, even though visually, it appears to be.

The implications of this are significant because it means that that any events that originate at #elem2 will not propagate (or bubble) up to the #elem1. You might very well want them to though.

For example, suppose you have an click event handler wired to #elem1 like so:

var elem1 = document.getElementById('elem1');
elem1.addEventListener('click', elem1Click, false);      // or your favorite addEvent(..) equalizer
function elem1Click(e)
{
   alert(e.target + ' was clicked');               // e.srcElement for IE
}

Clicking on #elem1 will cause an alert, but clicking on #elem2 won't, because #elem1 is not an ancestor of #elem2 and therefore, when the event that is dispatched from the 'click' event bubbles, it won't go through #elem1.

So how do you make the event go through #elem1? How you ensure that when a user clicks on #elem2 the 'click' handler for #elem1 fires? Through event re-routing!

An event detour

It's possible to manually dispatch an event in Javascript, and as usual, there are two (modern) ways to do it: The W3C DOM way, and the IE way. We'll start with the W3C method.

.dispatchEvent(evtObject)

The .dispatchEvent(..) method takes only one argument: the event object that you wish to dispatch. Sounds simple enough, but unfortunately it gets more complicated.

In order to dispatch an event, you must first create one. You do this through document.createEvent(evtType). But you're still not done. The event object you just created needs to be initialized, which can be done in a number of ways.

Here is a bare-bones example of creating and initializing a mouse event:

var myEvt = document.createEvent('MouseEvents');
myEvt.initEvent(
   'click'     // event type
   ,false     // can bubble?
   ,true      // cancelable?
);

At this point, you can go ahead and dispatch that lovely "click" event you just created/initialized to some DOM element of your choosing.

document.getElementById('myElem').dispatchEvent(myEvt);

And if all goes as planned, any "click" handlers wired to #myElem should fire.

This is all fantastic, but it leaves a number of important questions unanswered. Where was the mouse when the click occurred? Was the shift key depressed at the time? How about the ctrl key or the alt key?

To include this kind of meta-data with our event object, we need to change the syntax a bit, and supply a whole boat-load of additional parameters.

Here's an example.

var myEvt = document.createEvent('MouseEvents');
myEvt.initMouseEvent(
   'click'          // event type
   ,true           // can bubble?
   ,true           // cancelable?
   ,window      // the event's abstract view (should always be window)
   ,1              // mouse click count (or event "detail")
   ,100           // event's screen x coordinate
   ,200           // event's screen y coordinate
   ,100           // event's client x coordinate
   ,200           // event's client y coordinate
   ,false         // whether or not CTRL was pressed during event
   ,false         // whether or not ALT was pressed during event
   ,false         // whether or not SHIFT was pressed during event
   ,false         // whether or not the meta key was pressed during event
   ,1             // indicates which button (if any) caused the mouse event (1 = primary button)
   ,null          // relatedTarget (only applicable for mouseover/mouseout events)
);

// now dispatch the event
document.getElementById('myElem').dispatchEvent(myEvt);

The first thing you should notice is the use of .initMouseEvent(..) instead of just .initEvent(..), the former taking a much more complete set of parameters with which to initialize the event.

Generally speaking, you wouldn't do what I did in my example here. The parameters I passed in were pretty arbitrary (100, 200 for screen coordinates and such). Instead, if you're re-routing an event, you'll probably want to copy the properties of the original event. That would look something like this (comments omitted this time for brevity).

function clickHandler(e)
{
   var myEvt = document.createEvent('MouseEvents');
   myEvt.initMouseEvent(e.type, e.bubbles, e.cancelable, window, e.detail,
      e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey,
      e.metaKey, e.button, e.relatedTarget);

   document.getElementById('someOtherElem').dispatchEvent(myEvt);
}

.fireEvent(evtType, [evtObject])

IE handles manual event dispatching a little differently. You're not forced to create an event object to pass into .fireEvent(..). That second parameter is optional actually. Something like this would be acceptable (though it still begs the same question that using .initEvent(..) does):

document.getElementById('myElem').fireEvent('onclick');     // don't forget the "on"

This would simply dispatch a "click" event to #myElem with a set of default values based on the current event object.

If you want change some property of the event object (e.g. you want to force the event object to interpret the ALT key as having been depressed), then you need to create a new event object, which is done with document.createEventObject([e]), and pass that into .fireEvent(..) as the 2nd parameter.

function clickHandler(e)
{
   // base this new event on the existing event object, e
   var myEvt = document.createEventObject(e);

   // force the ALT key to appear as having been pressed
   myEvt.altKey = true;

   document.getElementById('someOtherElem').fireEvent('onclick', myEvt);
}

Now, when clickHandler(..) executes, it will forwards its event (or more correctly, create a new event based on the current event and dispatches it) to #someOtherElem while also simulating that the ALT key was pressed.

One Caveat

You might wonder why we have to create a new event instead of just truly forwarding on the existing event object (I wondered this). To be honest, I don't know. All I know is that in Firefox, this didn't work. It generated one of those cryptic nsComponentSomethingOrOther errors, whereas creating a new event worked like a charm.

This might not be a problem in other browsers, but I didn't bother to check, since not supporting Firefox is never an option for me.

An Event Re-routing Demo

To drive home the things I've been talking about in this post, I've put together a simple demo involving two sibling <div> elements, one of which will forward its "click," "mousemove," and "mousedown" events to the other. To make the demo a little more interesting, I'll make it so that forwarded "click" events appear to have the SHIFT key pressed.

View the event re-routing demo

Comments, questions, suggestions, complaints? Speak thy mind!