Export (0) Print
Expand All

Jurassic Dude

As of December 2011, this topic has been archived. As a result, it is no longer actively maintained. For more information, see Archived Content. For information, recommendations, and guidance regarding the current version of Internet Explorer, see Internet Explorer Developer Center.

Michael Wallent
Microsoft Corporation

September 6, 1999

Contents

Making It Animate
Draggin' and Droppin'
Summary

One of the fossils most often found from the Paleolithic DHTML area is the Expandus-Collapsum listus. Their changing folder images, as well as their cunning ability to arbitrarily hide and show groups of content, made them one of the most prolific examples of early DHTML life. The larger and more advanced Treeum VBicus and even more terrifying Namespacicus rex challenged these small creatures. The E.C. listus didn't have the agility to compete with their evolved features, such as animation or the widely used drag and drop. But something wonderful happened. The E.C. listus evolved. (Believe me, there were no intelligent designers here.) It got stronger. It became more and more powerful. This recent example shows how the E.C. listus uses its newly evolved skills.

You can see this more advanced E.C. listus right here.

Making It Animate

In the earliest known examples, the expanding and collapsing list utilized the display property to hide and show their children. While effective, this method is somewhat abrupt. First you see the content, and then you don't. There is no animation or transition. The display property doesn't really help with the expand/collapse animation at all. One way to make this work is to manage the size of the container rectangle. However, in order to have the elements in the rectangle clip, the overflow property needs to be set to hidden. Now, when the size of the container is smaller than the size of the content, the content that doesn't fit will be clipped.

To make it more obvious as to the function of each of the elements of the list control, I used XML elements, and named them by their function. Here's a snippet of the list declaration.

<ie:tree id=TheTree>
  <ie:treeitem><ie:label id=Label1>1.0 First Set</ie:label>
     <ie:treeitem><ie:label>1.1 Megalosaurus</ie:label></ie:treeitem>
     <ie:treeitem><ie:label>1.2 Iguanodon</ie:label></ie:treeitem>
     <ie:treeitem><ie:label>1.3 Hylaeosaurus</ie:label></ie:treeitem>
     <ie:treeitem><ie:label>1.4 Tyrannosaurus</ie:label></ie:treeitem>
  </ie:treeitem>
</ie:tree>

To collapse a set of children, the parent <ie:treeitem> element's height is animated from its full size, until it's just tall enough to hold the parent's label.

Here's how that code works:

// toggles expanding/collapse state of an element - does setup
function toggleState(e) {
   e.oHeight = e.scrollHeight + 2;
   e.style.posHeight = e.offsetHeight;

   if (e.scrollHeight >= e.offsetHeight) {
      growIt(e);
   } else {
      shrinkIt(e);
   }
}

The toggleState() method first stores the total height of the element in an expando property for future use. The height of the container is then set. Only when an element is being collapsed or expanded, or when the container is in the collapsed state, is the height set. As we'll see later, this makes using the container as a drop target for new elements more convenient, as the height will automatically grow as new elements are added.

Another change to note is how the determination to expand or collapse the list is made. The element.scrollHeight property always contains the "true" height of an object. This property reports how much size the element would take up, if its height weren't constrained. The offsetHeight property reports the actual current size of the element. So, if the scrollHeight is greater than the offsetHeight, we know that the list is in a collapsed state, and must be expanded. If the scrollHeight is less than the actual height, the list should be collapsed.

// called to initiate shrinking an element
function shrinkIt(e) {
  currCount = 0;
  window.setTimeout("doShrink(" + e.uniqueID + ");", msecs);
}

As I've written many times before, the appropriate way to change any property over time is with a setTimeout() construct, and not with a for() loop. One change in this method is the use of the element.uniqueID property. We want to call doShrink on a specific object, so we want to pass it as a parameter. We could have stored the "current object" in a global variable, but this method provides better encapsulation and state management. The reason to use element.uniqueID over element.ID is that every element has a uniqueID by default, where IDs are added by the page author. Using uniqueID is a guaranteed way to address an element. Also note that in a setTimeout() method, a reference to the ID or uniqueID of an element will cause the element itself to be passed as a parameter when the timer expires.

// inner loop for shrinking an object
function doShrink(e) {
  var dh, dw;
  var lineHeight = e.children[0].offsetHeight;
  var p;

  currCount++;

  dh = (e.oHeight - lineHeight) / flyCount;

  e.style.posHeight -= dh;

  if (currCount < flyCount) {
        window.setTimeout("doShrink(" + e.uniqueID + ");", msecs);
  }
  else {
    e.style.posHeight = lineHeight;
  }
}

Nothing really new here - just incrementing the height of the object over time.

The only changes for expanding the list are the new growIt() and doGrow() method. Here's the code for doGrow():

// inner loop for growing an object
function doGrow(e) {
  var dh;
  var lineHeight = e.children[0].offsetHeight;

  currCount++;

  dh = e.oHeight / flyCount;


  if (e.style.posHeight != e.oHeight) {
    e.style.posHeight += dh;
  }

  if (currCount < flyCount) {
      window.setTimeout("doGrow(" + e.uniqueID + ");", msecs);
  }
  else {
      e.style.height = "";
  }
}

The only difference between this method and the doShrink() method is what happens when the element has reached the intended size. As I mentioned above, when an element is in the expanded state, we don't want to constrain the height, as this will cause new elements added to the container to be clipped. Or, as elements are removed, gaps would appear at the end. We want the size of the container to grow as new content is added. As a result, when the element has reached full size, this method actually clears the e.style.height property. This doesn't set the height to zero. Clearing this property lets the element take as much height as it needs. As more elements are added (or removed), the height will adjust accordingly.

Draggin' and Droppin'

Another feature that more advanced list controls support is drag-and-drop functionality. With drag-and-drop capability, you can reorder items and subtrees within the list. One of the most striking things about writing drag-and-drop code is how many events come into play. Here are all the events this sample is handling:

// event handler hookup
TheTree.onmousedown = preselect;
TheTree.ondragstart = dragstart;
TheTree.ondragend = dragend;
TheTree.ondragenter = dragover;
TheTree.ondragover = dragover;
TheTree.ondragend = dragend;
TheTree.ondrop = dragdrop;

The actual code for a drag-and-drop operation is pretty simple once you get past all the events. One gotcha that I ran into right away had to do with how the drag begins. It turns out that only images can be dragged without being selected. Before anything else can be dragged, it needs to be text selected. I got around this restriction with this chunk of code:

function preselect() {
  var e;

  e = window.event.srcElement;
  if (e.tagName != "label") {
    return;
  }
  r = document.body.createTextRange();
  r.moveToElementText(e);
  r.select();
  window.event.cancelBubble = true;
}

This code is called onmousedown. It performs a manual selection, so that the thing the mouse went down on can be dragged. This is a limitation we're going to address in the browser very soon.

Starting the drag is pretty simple.

function dragstart() {
  var e;
  e = window.event.srcElement;
  if (e.tagName != "label")
    return false;

  dragElement = e.parentElement;
}

If you return false from this method, the drag is cancelled. This method is storing the element being dragged in the dragElement global. I decided not to use the clipboard part of the drag-and-drop operation—only the visual cues, as this application is designed to work within a single window.

As you move the mouse over an element during a drag operation, you get an ondragenter event. This is analogous to the onmouseover event. You get ondragover events as the mouse is moving within that object. This is analogous to the onmousemove event. You also get the ondragleave event when the mouse moves out. This is analogous to the onmouseout event. However, the onmouseout event supports the event.toElement and event.fromElement properties, which allow you to detect the case when an onmouseout event has been thrown, but you have just moved over one of your children. For example if you had:

<DIV ID=A>Text <IMG ID=B> Text </DIV>

As you move your mouse over the <DIV>, the onmouseover event is thrown on A. However, once you move your mouse over B, you first get an onmouseout for A, followed by an onmousever for B. With these mouse events, you can be "over" only one element at a time. However, the typical way to detect if you aren't really "out"—but just over a child—is with the following:

if (!A.contains(window.event.toElement) {
    alert("really out");
} else {
    alert("false alarm");
}

Unfortunately, the ondragleave event doesn't provide event.toElement and event.fromElement (another problem we'll fix soon). But in the interim, you can track this yourself. Here's my handler for both ondragover and ondragenter.

function dragover() {
  if (window.event.srcElement.tagName != "treeitem")
    return;

  if (!dragElement.contains(window.event.srcElement)) {
    if (currOver) {
      currOver.style.borderTopColor = "";
    }
    currOver = window.event.srcElement;
    window.event.srcElement.style.borderTopColor = "black";
    event.returnValue = false;
  }
}

Since I'm manually managing the object that the mouse is currently over (currOver), I change that object only when the object that we're currently over isn't contained in the last object we were over.

Another important note here is that the return value of this method controls the drag cursor. If you return true or no value, the default cursor is shown. The default cursor allows a drop on an editable region, such as an input field or a text area. If this method returns false, the allow-drop cursor is shown.

The other function of this method is to show the visual cue for where the element will be dropped. I chose to use the top border of an element to show the drop location. The style sheet for <ie:treeitem> specifies a top border, but no color. This means that the border is counted in the layout, so that as the border color goes from "none" to "black", you don't get "border bounce", a phenomenon that occurs when the element gets bigger or smaller as the border comes and goes. So, as we go over an element, its border color is changed to "black", and as we move off, the border color is set back to "".

Once the drop occurs, moving the element is trivial.

function dragdrop() {
  if (currOver) {
    currOver.style.borderTopColor = "";
  }
  t = currOver;
  t.parentElement.insertBefore(dragElement, t);
}

The color on the border is removed, and the dragElement global is inserted before the current object that the mouse is over.

There's one more little feature in this sample. I wanted an easy way to change the style of an element if it had children. I didn't want to have to manage a hasChildren property on each object, because with the drag and drop functionality, the number of children on an element can change (from some to none). A dynamic property was perfect here. For elements with children, they show bold text. For leaf nodes, they show normal text. Here's the code for that.

    ie\:treeitem {font-weight: expression(this.children.length > 1 ? 'bold' : 'normal');}

If you take the "4.1" node, and move it somewhere else, you will see the font size change automatically, with no manual intervention.

Summary

I hope you liked our paleontological dig for this month. I'll be back next month with another fossil or two.

 

DHTML Dude

Michael Wallent is Microsoft's group program manager for Internet Explorer.


  
Show:
© 2014 Microsoft