/workshop/author/
Michael Wallent
Microsoft Corporation
May 3, 1999
The following article was originally published in the MSDN Online Voices "DHTML Dude" column.
Remember Soylent Green ?
"Soylent green is made of people! You've got to tell them!"
Well, it's kind of like the Web -- except, instead of being made of people, the Web is made of tables. Yes, that's right: It's made of tables. Look at the sites you browse day in, day out. View the source code, search for <TABLE>, and I'll be willing to bet you'll find at least one -- if not two or more -- of these nasties.
Unfortunately, back in the days of Internet Explorer 4.0, trying to manipulate a table using DHTML was--well, let's just say it didn't quite work. One of my personal favorites--manipulating the display property--left little turds (yes, that's a technical term) of border after the display was closed. Try to add or remove a row or cell? I think two testers in building 10 have that figured out how to combine incense, chanting, and frog legs to make it work, but it's a dark, complex art (and a pretty smelly process).
Too hard. Too complex. Didn't work. Forget it.
Now for the good part: With Internet Explorer 5, the display property works on table rows and cells. Also, it's a piece of cake to insert and remove table cells and rows. It's also pretty easy to move them around. And cake is much nicer than frog legs.
The table editor allows the user to select either a single cell, with a click. Or, by pressing ALT and clicking, an entire row can be selected. Let's see how that's done:
function select(element) { var e, r, c; if (element == null) { e = window.event.srcElement; } else { e = element; } if ((window.event.altKey) || (e.tagName == "TR")) { r = findRow(e); if (r != null) { if (lastSelection != null) { deselectRowOrCell(lastSelection); } selectRowOrCell(r); lastSelection = r; } } else { c = findCell(e); if (c != null) { if (lastSelection != null) { deselectRowOrCell(lastSelection); } selectRowOrCell(c); lastSelection = c; } } window.event.cancelBubble = true; } TableContainer.onclick = select; function cancelSelect() { if (window.event.srcElement.tagName != "BODY") return; if (lastSelection != null) { deselectRowOrCell(lastSelection); lastSelection = null; } } document.onclick = cancelSelect; function findRow(e) { if (e.tagName == "TR") { return e; } else if (e.tagName == "BODY") { return null; } else { return findRow(e.parentElement); } } function findCell(e) { if (e.tagName == "TD") { return e; } else if (e.tagName == "BODY") { return null; } else { return findCell(e.parentElement); } } function deselectRowOrCell(r) { r.runtimeStyle.backgroundColor = ""; r.runtimeStyle.color = ""; } function selectRowOrCell(r) { r.runtimeStyle.backgroundColor = "darkblue"; r.runtimeStyle.color = "white"; }
To capture all the click events for the table, I've hooked the TableContainer.onclick event to my select() method. However, I'm actually going to use the select() method in other places in my code. For example, when a new row is created, I'd like to be able to select it after it's inserted. To that end, the select() method takes an optional parameter--choosing the element to be selected.
Most of the other code in the select() method is selection/deselection logic, to ensure that only a single row or cell is visually highlighted at once.
Note that the srcElement of the click handler is not assumed to always be the actual table cell (<TD>). If the user clicks on text that's inside the cell, then the srcElement will be the <TD>; however, if there are more complex HTML constructs, such as <TD><B>Some Bold</B> Text</TD>, clicking on "Some Bold" will return a different srcElement than "Text". To this end, the findCell/findRow methods are used to recursively search up the parentElement chain to find the containing <TR> or <TD> for each click that occurs.
The selection process also uses some new functionality in Internet Explorer 5. When a cell or row is "selected", it should appear visually distinct. This can easily be accomplished by setting the cascading style sheets (CSS) color and background-color properties. However, if I set element.style.color, the selection process would effectively override any user-set CSS properties (set with the Set Row/Cell Style function). To make it so the user can set CSS properties on a cell or on a row, and my programmatic manipulations of color don't interfere, I'm actually using an object called runtime style.
Styles set through the runtime style object (element.runtimeStyle) override all other CSS property value settings. Because the runtimeStyle object is kept separate, changes will not conflict with other CSS settings. Take, for example, the following sequence:
element.style.color = "blue"; // element is now blue element.runtimeStyle.color = "red"; // element is now red element.runtimeStyle.color = ""; // element is blue again
Note how changes made to runtimeStyle override other CSS settings on an element; but when the object is removed, the initial CSS values are reapplied.
Adding any HTML or XML elements is significantly easier with Internet Explorer 5. With Internet Explorer 4.0, elements had to be added by inserting their HTML representation into the page:
document.body.insertAdjacentHTML("AfterBegin", "<img src='ie.gif'>");
Some elements couldn't even be created this way. Table elements and framesets, as well as elements in the head of the document, couldn't be easily manipulated.
With Internet Explorer 5, creating these elements is simple and straightforward. You don't need to understand the HTML syntax to insert new elements into a document.
i = document.createElement("IMG"); i.src = "ie.gif"; document.body.insertBefore(i, null); Here's the code that adds a new cell into a table row: function addCell() { var r, p, c, nc, text; if (lastSelection == null) return false; r = lastSelection; if (r.tagName == "TD") { r = r.parentElement; c = lastSelection; } else { c = null; } nc = document.createElement("TD"); text = document.createTextNode("New Cell"); nc.insertBefore(text, null); r.insertBefore(nc, c); select(nc); return nc; }
The first part of this method simply deals with finding the current selection. However, the last five lines are the crux. The new <TD> is created using the createElement() method. It's important to understand that elements created this way aren't visible in the document until they have been parented to an element in the live document. It's possible to aggregate trees of elements externally to the document. (insertBefore() can be called on elements not in the main tree). In that way, entire sections of a document can be added (or removed) at once.
In this case, I wanted the new cell to contain some text. To create or manipulate text when the elements are outside of the main document tree, you must use Text Nodes. Text Nodes contain only text--not other HTML elements. If I wanted my <TD> to contain a bold tag that contained text, I would have created a <B> element, and inserted the <B> into the <TD>, and the text into the <B>. To find out more about text node objects, check out the DHTML Objects area of the Web Workshop.
Note that I'm using the select() method described in the previous section to select the newly created table cell.
As with most things, it's easier to destroy than to create. Here's the code for removing a table row.
function removeRow() { var r, p, nr; if (lastSelection == null) return false; r = lastSelection; if (r.tagName == "TD") { r = r.parentElement; } p = r.parentElement; p.removeChild(r); lastSelection = null; return r; }
A single method -- removeChild() -- can be used to remove any element, or subtree of elements, from the document. Note that removing an element in this way from the tree doesn't delete it. Elements that have been removed are in the same state as newly created elements. They can be reinserted into the document in a different location at any time. From a script point of view, these elements are actually deleted when there are no more references to them.
We've seen how the insertElement() method can be used to place a new element into an existing document. It can also be used to relocate an element as well. It is valid to call insertBefore() on an element already in the tree. By doing so, you will remove the element from its current location, and automatically move it to the newly specified place. This is analogous to (but shorter than) calling removeChild() and then insertBefore().
Here's the code for moving a table cell one location to the left:
function moveLeft() { var c, p, ls; if (lastSelection == null) return false; c = lastSelection; if (c.tagName != "TD") { return null; } ls = c.previousSibling; if (ls == null) return null; p = ls.parentElement; p.insertBefore(c, ls); return c; }
To move a cell to the left, simply insert the cell before its sibling to the immediate left. Two new properties are used to navigate your siblings: previousSibling and nextSibling. Note that the moveRight() method is very similar, but uses the nextSibling property instead.
As an exercise left to the reader, how would you write moveLeft() so that it would also work on table rows? Is there a common code method between moveLeft() and moveUp()?
Back when I first saw Lotus 1-2-3, one of the features that impressed me most was that the values in the cells were immediately updated as you typed. (Okay, so that was 1990.) That feature is still pretty hard to accomplish even today.
However, a new feature of Internet Explorer 5 makes the process a breeze. With Dynamic Properties, it's a three-liner:
function editContents() { var c, p, nr; if (lastSelection == null) return false; c = lastSelection; if (c.tagName != "TD") { return null; } EditCell.style.display = ""; EditCell.value = c.innerHTML; c.setExpression("innerHTML", "EditCell.value"); EditCell.focus(); EditCell.onblur = unhookContentsExpression; } function unhookContentsExpression() { lastSelection.removeExpression("innerHTML"); EditCell.value = ''; EditCell.style.display = "none"; }
By using Dynamic Properties, the innerHTML property of the cell is defined to be the value of the input. As the value of the input changes, the cell contents automatically change. The only event that needs to be watched is the onblur event, so that this relationship can be severed. No keystroke watching is required.
Did you notice how the buttons on the page enable and disable as different types of elements are selected? This type of operation is usually done with an idle loop process, or by tracking all the places in the application where the state changes and making the enable/disable decision. What a mess. However, Dynamic Properties makes this easy.
ButtonAddRow.setExpression("disabled", "nothingSelected(lastSelection)"); ButtonMoveRight.setExpression("disabled", "! cellSelected(lastSelection)"); ButtonEditContents.setExpression("disabled", "(! cellSelected(lastSelection)) || (EditCell.style.display == '')"); ButtonEditStyle.setExpression("disabled", "(EditStyle.style.display == '')"); ButtonEditStyle.setExpression("value", "'Edit ' + whatIsSelected(lastSelection) + ' Style'");
I created a set of three helper methods: nothingSelected(), cellSelected(), and rowSelected(). The disabled property for each of the buttons is set up as a Dynamic Property, so that the expression will be true when the button should be disabled. For example, "Move Right" operates only on cells, so that when a cell isn't selected, the button should be disabled.
Note that the lastSelection property is sent as a parameter to all these methods. Because this property is referenced in the expression, a dependency is set up; whenever that property changes, the expression is reevaluated. No matter how or where in the code lastSelection changes, all of the buttons automatically enable and disable with no additional code or overhead.
Hopefully, the examples above will help you make your interactions with HTML tables less painful. If you have any questions or any suggestions for future columns, please feel free to drop me a note at msdn@Microsoft.com. I'd be glad to help. 'Till next month.
Michael Wallent is Microsoft's group program manager for DHTML.