reptile7's JavaScript blog
Monday, October 08, 2012
Rolling in the Div
Blog Entry #266

In today's post we will wrap up our analysis of the Expanding Colored Squares Example of Netscape's Dynamic HTML in Netscape Communicator resource, more specifically, we'll finish modernizing the example's JavaScript and then roll out a couple of demos.

The mouseover blues

As detailed earlier, both expansion and contraction of the example's colored squares are triggered by mouseover events. With modern browsers, mouseover and mouseout events can be difficult to disentangle, a situation we previously addressed in the Stopping and restarting the animation section of Blog Entry #234; in this regard there are three mouseover issues that we need to deal with:
(1) Like all mouse events, mouseover events are dispatched to the most deeply nested element; if a mouseover currentTarget (the object to which the onmouseover event handler is actually bound) is an ancestor of the most deeply nested element, mouseover propagation does not stop at the currentTarget.
(2) Mouseover events bubble.
(3) A mousemove that (a) is entirely within a mouseover currentTarget and
(b) either enters or leaves a descendant of the currentTarget
fires a mouseover event for the currentTarget.

Suppose we mouseover the example's 1 colored square and then let the mouse cursor sit. As soon as the mouse cursor enters the square, a mouseover event is dispatched to the topleftblock div element (the event's currentTarget, as set in the example's HTML), the changeNow( ) and expand( ) functions are called, and the square expands as described in Blog Entry #264. What happens next depends on where we move the mouse cursor.

If we move the cursor rightward to the 2 square, then the 2 square will expand and the fully expanded topleftblock square

The fully expanded topleft colored square

will remain expanded; ditto if we move the cursor downward to the 3 square. But if we keep the cursor in the topleftblock square and move it northwestward and over the Netscape Navigator 4.0 is the newest version of Netscape's world-leading software for browsing information on intranets or the Internet. paragraph, then the mouseover event dispatched to the underlying p element will bubble up to the topleftblock div element, the changeNow( ) and contract( ) functions will be called, and the square will contract back to its original state, and most users will find this annoying (or at least I find it annoying). Try it out in the div below:


This problem does not arise when running the original example with Netscape 4.x, which does not support onmouseover and onmouseout for the p and h# elements and, more fundamentally, does not support event bubbling in the first place; we can move the cursor anywhere within the expanded topleftblock square and contraction does not occur. Moreover, Netscape 4.x does not interpret internal child-to-parent mouse movement as a mouseover event for the parent; if we replace the Netscape Navigator 4.0 is ... paragraph with a link, for which Netscape 4.x does support onmouseover and onmouseout, then moving the cursor from the link to a contentless part of the topleftblock layer does not call the changeNow( ) function.

Enter the mouseenter

We can't exactly duplicate the Netscape 4.x 'mouseover model' with modern browsers as these browsers do support onmouseover for the p and h# elements (and most other document body elements), but we can come pretty close. Microsoft has introduced an onmouseenter event (handler) that fires [if and] only if the mouse pointer is outside the boundaries of the object and the user moves the mouse pointer inside the boundaries of the object. Mouseenter events do not bubble and are not dispatched by internal parent-to-child or child-to-parent mouse movement when onmouseenter is bound to the parent.

The onmouseenter event handler was supported by IE, Firefox, and Opera when this post was first written (it's now also supported by Google Chrome and Safari). According to Quirksmode, onmouseenter's IE support goes back to IE 5.5; according to Mozilla, onmouseenter's Firefox support began with Firefox/Gecko 10 and its Opera support began with Opera 11.10. As of this writing, the mouseenter event is not standard but is on track to be.

So how do we sort out those users that have onmouseenter support and those that don't? According to JavaScript Kit, we can flag IE 5.5+ users with a window.createPopup condition. (Let's hope no one out there is using a pre-5.5 version of IE, but you never know, do you?) However, I have no idea how to test for Firefox 10+ and Opera 11.10+. And how are we going to accommodate users with onmouseover support but not onmouseenter support?

Mouseover to mouseenter, sort of

Stephen Stchur has posted a way to 'convert' mouseover events to mouseenter events for browsers that support the Events DOM's addEventListener( ) method. Stephen's code uses a mouseover event's relatedTarget, the element that the mouse cursor just exited, to conditionalize the execution of the event's listener; if the relatedTarget is an ancestor or a sibling of the event's currentTarget, then the listener is executed, but if the relatedTarget is a descendant of or equal to the currentTarget, then nothing happens, as for a mouseenter event. The code comprises three functions:

function addEvent(_elem, _evtName, _fn, _useCapture) {
    if (typeof _elem.addEventListener != "undefined") {
        if (_evtName === "mouseenter") _elem.addEventListener("mouseover", mouseEnter(_fn), _useCapture);
        ... }
    else if (typeof _elem.attachEvent != "undefined") _elem.attachEvent("on" + _evtName, _fn);
    else _elem["on" + _evtName] = _fn; }

function mouseEnter(_fn) {
    return function(_evt) {
        var relTarget = _evt.relatedTarget;
        if (this === relTarget || isAChildOf(this, relTarget)) { return; }, _evt); } }

function isAChildOf(_parent, _child) {
    if (_parent === _child) { return false; }
    while (_child && _child !== _parent) { _child = _child.parentNode; }
    return _child === _parent; }

We're not going to go through the functions line by line (doing so would be worthwhile but would take another post); here are the highlights:

• The isAChildOf( ) function compares a mouseover's currentTarget (this) and relatedTarget (relTarget); it returns true if the relatedTarget is a descendant (not just a child) of the currentTarget and false otherwise.

• The mouseEnter( ) function creates and returns an anonymous function that calls an _fn function if (a) the mouseover's currentTarget and relatedTarget are not the same and (b) the relatedTarget is not a descendant of the currentTarget.

• The addEvent( ) function registers mouseEnter( )'s anonymous function on an _elem element; for the benefit of IE 5-8 users, it also includes an else if clause that registers the _fn listener on the _elem element via Microsoft's proprietary attachEvent( ) method.

Other points:

• The typeof _elem.addEventListener != "undefined" condition can be simplified to _elem.addEventListener and the typeof _elem.attachEvent != "undefined" condition can be simplified to _elem.attachEvent.
(I was going to nigglingly point out that in JavaScript undefined is a primitive data type, is not a string, and shouldn't be quoted, but at least some versions of IE throw an error if the undefined operand is not quoted, so never mind.)

• The strict ===/!== operators should in most cases be replaceable by their not-so-strict ==/!= counterparts.

• It is not necessary to invoke the call( ) method of the Function object to call the _fn function; a basic _fn( ); command will suffice. (Cf. the factorial function example near the end of the Defining functions section of Mozilla's JavaScript Guide.)

• Note that currentTarget-relatedTarget equality is tested twice: one test will do if we move the _fn call to the anonymous function's if clause
if (isAncestorOrSibling(this, relTarget)) _fn( );
and conclude the 'child-testing' function (let's rechristen it isAncestorOrSibling( )) with a
return (!_child ? true : false);

• Stephen notes that the anonymous function constitutes a closure; closures are discussed by Mozilla here. I share Stephen's 'closures are not a big deal' attitude: I don't find it unintuitive at all that the anonymous function is still able to act on the _fn function once the mouseEnter( ) function has finished executing.

All that remains for us to do is to set up addEvent( ) function calls for the parent divs:

window.onload = function ( ) { ...
    addEvent(squares[0], "mouseenter", function ( ) { changeNow(0); }, false);
    addEvent(squares[1], "mouseenter", function ( ) { changeNow(1); }, false);
    addEvent(squares[2], "mouseenter", function ( ) { changeNow(2); }, false);
    addEvent(squares[3], "mouseenter", function ( ) { changeNow(3); }, false); }

(Does it make any difference as to whether the addEvent( ) _useCapture argument is true or false? Not in the present case, as far as I can tell - I've set it to false as false is the default value for the addEventListener( ) useCapture argument.)

Mouseenter demo

I can't guarantee that the following demo, which incorporates the mouseover-to-mouseenter code of the previous section, will work for you, but give it a go. Move your mouse cursor over a colored square to expand it; when expansion is complete, mousing over the square's paragraph or h3 heading gives a relatedTarget == currentTarget situation and the square should remain expanded. To contract the square, mouseout from it and then mouseover it again.


The common syntax for an expansion or contraction step is:

squares2[n].style.clip = "rect(" + squares2[n].ctop2 + "px, " + squares2[n].cright2 + "px, " + squares2[n].cbottom2 + "px, " + squares2[n].cleft2 + "px)";

Click demo

Maybe you would rather expand the squares by clicking a button - I think I would too, actually:


This demo dispenses with the changeNow( ) function and effects expansion/contraction of the squares via onclick="expand3(0); expand3(1); expand3(2); expand3(3);" and onclick="contract3(0); contract3(1); contract3(2); contract3(3);" button element attributes.

FYI: The if (squares3[n].status3 == "contracting") return; statement at the beginning of the expand3( ) function body and the if (squares3[n].status3 == "expanding") return; statement at the beginning of the contract3( ) function body are there to prevent the spastic display that would otherwise (in the statements' absence) result from clicking the button while the squares are expanding or from clicking the button while the squares are contracting (a normal user wouldn't do that, but not everyone is a normal user).

In the next entry we'll begin work on the Changing Wrapping Width Example, the final DHiNC example.

This Blog was most helpful, your ideas are straight to the point, and the colors are cool too.
Post a Comment

<< Home

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)