reptile7's JavaScript blog
Sunday, October 28, 2012
 
Drag to Resize, Part 2
Blog Entry #268

We return now to our analysis of the Changing Wrapping Width Example of Netscape's Dynamic HTML in Netscape Communicator resource.

Event, revisited

We previously dealt with mousemove capture at the level of the document object in our discussion of the HTML Goodies JavaScript Script Tip #91 Script in Blog Entries #107 and #108. In the Mozilla/W3C compatibility section of the latter entry I wrote in part:
Event (with a capital E) is not a reference for the event object that we've been variabilizing as ev; rather, Event is some sort of behind-the-scenes constructible core object: typeof Event returns function and document.write(Event) outputs a function Event() { [native code] } string [upon collapsing excess white space]. You won't find anything about Event, whatever it is, in any of Netscape's or Mozilla's materials; my bet is that, like the captureEvents( ) method, Event is now obsolete.
That was back when I was working in the 'Classic Mac realm', so I thought it prudent to rerun a window.alert(Event); test with the modern GUI browsers on my Intel Mac to see if any of them recognized the Event function: all of them did, variously returning [object Event], function Event() { [native code] }, [object EventConstructor], or [Event]. Rerunning the typeof Event test gave function with most of these browsers but did give object with Safari, which inspired me to go after Event with a for...in loop. (Although every function in JavaScript is a Function object, my attempts to probe other typeof-designated functions with a for...in loop have been uniformly unsuccessful.)

So it would seem that Event is not so obsolete after all. Indeed, in the course of looking up the captureEvents( ) method at the Dottoro Web Reference I learned that Event now references the Event interface of the Events DOM and that MOUSEDOWN, MOUSEMOVE, etc. are nonstandard "constants" of that interface; in corroboration, the Event interface's three standard constants - CAPTURING_PHASE, AT_TARGET, and BUBBLING_PHASE - always show up in my result += "Event." + i + " = " + Event[i] + "<br>"; for...in output, regardless of browser.

There's more I could say about this Event constant business, but given its dubious utility, let's just move on, shall we?

In practice

Both versions of the original example work well with Netscape 4.x for the most part. However, I find that Netscape 4.x's onmouseup support is a bit buggy: on occasion when I mouseup over the Netscape Communicator heading or one of the paragraphs, the enddrag( ) function is not called and the layer1 layer and its contents continue to expand or contract in response to mousemoves in the layer1 content area. Perhaps this is a result of the particular version of Netscape that I'm using (Communicator 4.61), it could be a Mac thing, maybe the Netscape guys themselves observed this and swept it under the rug, I don't know.

Modernize it

Go here to access the demo at the beginning of the previous entry - check the page source for the full coding.

Coding the example for modern browsers* is easier than it is for Netscape 4.x for the following reasons:
(a) Per the HTML 4.01 Specification, modern browsers allow us to bind onmousemove to a div element.
(b) Modern browsers support event bubbling and thus do not need to capture the mousedown, mousemove, and mouseup events, which all bubble.
(*And more than a few not-so-modern browsers: (a) and (b) are true for IE 4+ and Netscape 6+.)

To straddle the Netscape/Microsoft event model 'divide' we can:
(1) use an e = e ? e : event; conditional statement to give an event object a common e identifier for both event models; and
(2) replace e.pageX with e.clientX. (We are only interested in relative e.clientX readings and consequently it is unnecessary to 'correct' those readings by adding document.body.scrollLeft to them even if the document width exceeds the viewport width.)
(1) and (2) are needed for IE 5-8 users; IE 9+ supports the Netscape-cum-W3C event model and the pageX property - as of this writing, pageX is not standard but is on track to be added to the Events DOM's MouseEvent interface.

var layer1 = document.getElementById("layer1"), oldX;
layer1.onmousedown = begindrag;
function begindrag(e) {
    e = e ? e : event;
    oldX = e.clientX;
    layer1.onmousemove = drag; }


As for the example's HTML, I kept the wrapcss.htm layer1 div element and loaded the mytext.htm body element content - the hr element, the h1 element, the p element text - into it. The mytext.htm h1/p stylings were also brought into the main document.

The layer1 div element is the containing block for its constituent elements and thus sets the wrapping width for those elements. We can change the div's wrapping width by writing its style.width property:

var layerWidth = 300;
function drag(e) {
    e = e ? e : event;
    layerWidth = (layerWidth + e.clientX - oldX);
    layer1.style.width = layerWidth + "px";
    oldX = e.clientX; }


I saw no point in having separate drag( ) and changeWidth( ) functions so I brought the changeWidth( ) operations into the drag( ) function.

In theory, we should be able to change the wrapping width of the layer1 layer with a corresponding

document.ids.layer1.width = layerWidth + "px";

statement; in practice, document.ids.layer1.width can be read but not written - that's why we used the layer object's load( ) method for this operation.

Finally, here's my enddrag( ) code:

layer1.onmouseup = enddrag;
function enddrag(e) { layer1.onmousemove = null; }


The original enddrag( ) function disconnects mousemoves from the drag( ) function by setting layer1.onmousemove to 0; I set it to null because a top-level for...in probe of the layer1 div features a layer1.onmousemove = null line in its output with Firefox and Opera. Moreover, a top-level typeof layer1.onmousemove test returns object with newer browsers and undefined with some older browsers but in no cases does it return number. The original enddrag( )'s releaseEvents( ) operation is not relevant to modern browsers: our mousemoves are going to be dispatched come what may and we're not capturing them anyway.

And that concludes our tour of the DHiNC examples. What's next to go under the microscope? Right now I've got my eye on the JavaScript sector of Lissa Explains It All, which contains several scripts that (a) have cool effects but (b) are alleged to be "IE only" (not, as we'll see) - sounds right up our alley, eh? So that's what we'll tuck into in the following entry, beginning with the "snow fall on my Web page" script.

Friday, October 19, 2012
 
What an oldX It Is Getting drag( )ged
Blog Entry #267

The Dynamic HTML in Netscape Communicator examples conclude with the Changing Wrapping Width Example, which we take up in today's post. The Changing Wrapping Width Example presents code via which the user can increase or decrease the widths of a layer and the elements it contains by respectively dragging the layer rightward or leftward.

In the div below, move your mouse cursor into the bluish content area, hold down the primary mouse button, and move the mouse cursor to the right or left.

Resizing a layer/div and its contents


Netscape Communicator

Netscape Communicator is a suite of software components for sharing, accessing, and communicating information via intranets and the Internet.

Communicator includes components for navigation, email, discussion groups, HTML authoring, dynamic information delivery, real-time collaboration, calendaring and scheduling, IBM host communications, and Communicator management.

Communicator runs on 16 platforms and is available in Standard and Professional Editions.

Netscape offered two versions of the Changing Wrapping Width Example:

(1) A wrapping.htm version puts the Netscape Communicator content in a layer element.

<layer name="layer1" src="mytext.htm" left="100" width="300" bgcolor="#99bbff"></layer>

(2) A wrapcss.htm version puts the Netscape Communicator content in a div element.

#layer1 { position: absolute; include-source: url("mytext.htm"); left: 100px; width: 300px; background-color: #99bbff; border: 1px solid white; }
...
<div id="layer1"></div>


The Netscape Communicator content comes from an external mytext.htm file that is imported into the layer1 layer element via its src attribute and into the layer1 div element via its include-source CSS property. The mytext.htm document body comprises a horizontal rule, an h1 heading, and three paragraphs.

The two versions' JavaScript parts are almost identical - I'll note where they differ in the following section.

Script overview

The example JavaScript relies on several functions to do its work - we'll take 'em in chronological order. Holding down the primary mouse button in the layer1 content area calls a begindrag( ) function.

var layer1 = document.layers["layer1"], oldX;
layer1.document.onMouseDown = begindrag;
function begindrag(e) { ...
    layer1.document.onmousemove = drag;
    oldX = e.pageX; ... }


The begindrag( ) function coassociates the layer1 layer's document, mousemove events, and a drag( ) function, and it also gets the mouse cursor's starting x-coordinate and assigns it to an oldX variable.

• The onMouseDown event handler appears as onMouseDown in the wrapping.htm source and as onmousedown in the wrapcss.htm source; modern browsers require an all-lowercase formulation for this type of statement but the lower camel case formulation is OK with Netscape 4.x.

• It might not be intuitively obvious to you that layer1.document provides access to the imported mytext.htm document - recall that an iframe uses a specialized contentDocument property to get at its src document - but it does do that.

e.pageX returns the cursor's x-coordinate relative to the left edge of the page (as opposed to the left edge of the layer1 layer).

With the primary mouse button held down, moving the mouse cursor in any direction calls the aforementioned drag( ) function.

function drag(e) {
    changeWidth(layer1, e.pageX - oldX);
    oldX = e.pageX; ... }


The drag( ) function's first order of business is to call a changeWidth( ) function.

var layerWidth = 300;
function changeWidth(layer, delta) {
    layerWidth = layerWidth + delta;
    if (delta != 0) layer.load("mytext.htm", layerWidth); }


The drag( ) function passes two arguments to the changeWidth( ) function:
(1) the layer1 layer object, which is rechristened layer;
(2) e.pageX - oldX, the horizontal distance traversed by our mousemove, which is given a delta identifier - for a mousemove event, e.pageX returns the cursor's ending x-coordinate.

In the top-level (unfunctionized) part of the example JavaScript, the layer1 layer's width in pixels, 300, is assigned to a layerWidth variable. The first changeWidth( ) function body statement adds the delta distance to the layerWidth width. Subsequently an if statement checks if delta is not equal to 0, i.e., if there was a horizontal component to our mousemove: if true (the condition would return false for a purely vertical mousemove), then the mytext.htm file is reloaded into the layer layer and the layer layer's width is set to the new layerWidth via the load( ) method of the layer object - this is the line that actually resizes the layer1 layer and its contents when the layer is dragged.

In the wrapcss.htm source, the layerWidth += delta assignment is preceded by a layer.bgColor = "#99bbff"; statement; the load( ) command dispatches a load event that would discharge the #99bbff background color of the layer1 div element in the statement's absence. (Relatedly, the load event removes the div's solid white border.)

Moving back to the drag( ) function, oldX is set to e.pageX after the changeWidth( ) function has finished executing, setting the stage for our next mousemove.

The load( ) load event triggers a resetcapture( ) function when the drag( ) function has finished executing - we'll discuss the resetcapture( ) function in the following section.

Finally, releasing the primary mouse button calls an enddrag( ) function that uncouples mousemoves in the layer1 content area from the drag( )/changeWidth( ) action by setting the layer1 document's onmousemove event handler to 0.

layer1.document.onMouseUp = enddrag; /* It's onmouseup in the wrapcss.htm source. */
function enddrag(e) {
    layer1.document.onmousemove = 0; ... }


We are now free to move the cursor anywhere within the widened or narrowed layer1 area and the area will stay put. Or we can mousedown on and drag the area again as we see fit.

Capture voodoo

Omitted from the previous section's code are three document.captureEvents( ) commands that play important roles in the example's operation. Unfortunately, neither the example text nor the Event Capturing section of the JavaScript 1.3 Client-Side Guide sheds any real light on why these commands are required: I'll explain them as best I can.

The top-level part of the example JavaScript includes the following command:

layer1.document.captureEvents(Event.MOUSEUP|Event.MOUSEDOWN|Event.MOUSEDRAG);

JavaScript 1.2 equipped the document object with onMouseDown and onMouseUp event handlers so you might not think that we would need to capture mousedown and mouseup events for the layer1 document, and mousedown/mouseup capture is indeed unnecessary if the mouse cursor is maneuvered outside the layer1 h1 and paragraph text.

But suppose we mousedown right on the Netscape Communicator heading or one of the paragraphs. In the absence of capture, our mousedown will be dispatched to the underlying h1 or p element - the browser will see the mousedown as the beginning of a text selection process - but it will not bubble up to the element's layer1.document ancestor (recall that Netscape 4.x does not support event bubbling), so if we want to be able to mousedown anywhere within the layer1 area and call the begindrag( ) function so we can get the layer1 dragging under way, then we're going to have to intercept the mousedown at the layer1.document level.

It is actually not necessary to capture mouseup events at this stage, but we'll definitely need to capture them after we reload mytext.htm into the layer1 layer (vide infra).

The Event.ONMOUSEDRAG subargument initially threw me for a loop as there is no onMouseDrag in classical JavaScript's list of event handlers. Interestingly, however, the mysterious Event object/function (the generic/default Event object according to Danny Goodman although Netscape doesn't say anything about it in the JavaScript 1.2/1.3 specifications) can with modern browsers be probed via a for...in loop

var result = "";
for (var i in Event) result += "Event." + i + " = " + Event[i] + "<br>";
document.write(result);


and the result, which varies from browser to browser, does include an Event.MOUSEDRAG = 32 line when using Firefox, Google Chrome, or Safari. Still, I wondered, "Will anything bad happen if I subtract the Event.ONMOUSEDRAG subargument?" I took it out and there were no ill effects as far as I could tell.

Moving on, the begindrag( ) function body begins with:

layer1.document.captureEvents(Event.MOUSEMOVE);

The onMouseMove event handler was also introduced in JavaScript 1.2 but Netscape didn't associate it with any objects at all (onMouseMove is unique in this respect), so we'll need to capture mousemoves at the layer1.document level in order to register the drag( ) listener on the layer1 document. Indeed, Netscape states that mousemove events are not even dispatched by Netscape 4.x in the absence of capture.

As it happens, the preceding captureEvents( ) commands are only good for our first mousedown and mousemove; these commands are cleared by the load( ) operation of the changeWidth( ) function and must be repeated after the mytext.htm content has reloaded.

layer1.onload = resetcapture;
function resetcapture( ) {
    layer1.document.captureEvents(Event.MOUSEUP|Event.MOUSEDOWN|Event.MOUSEDRAG|Event.MOUSEMOVE); }


We are still mousedowning at the time of the first load( ) operation. If we want to mouseup over the h1 heading or a paragraph and call the enddrag( ) function, and not have the mouseup interpreted as the end of a text selection process, then the mouseup must be intercepted at the layer1.document level, and thus the Event.ONMOUSEUP subargument is needed here. The Event.ONMOUSEDRAG subargument can again be thrown out.

Release the mousemove

The enddrag( ) function sports a

layer1.document.releaseEvents(Event.MOUSEMOVE);

command that stops the dispatch of mousemove events when we move the mouse cursor. The releaseEvents( ) command makes the layer1.document.onmousemove = 0; statement unnecessary: if we're not dispatching mousemove events in the first place then there's no need to 'turn them off'.

In the following entry, I'll briefly comment on how the original example works in practice and then I'll tell you what I did to modernize the code.

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:

1
2
3
4

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; }
        _fn.call(this, _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);
statement.

• 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.

1
2
3
4

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:

1
2
3
4

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.


Powered by Blogger

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