Situation: The project has a requirement that the user can highlight the row and column for a particular cell in a table, and that they stay highlighted while the user navigates the page.
This the fourth How-to in a series on highlighting table cells, rows, and columns. Like the preceding How-to's, this one is intended to stand on its own. But, the series is progressive in its presentation and some details presented earlier may not be given as much coverage in this time. Therefore, reading—or at least skimming—the previous How-to's may help clarify some aspects of this How-to.
The requirements are slightly different this time. The highlighting is NOT be affected by the mouse rollover. The user needs to be able to highlight the column and row of a cell so they can easily read the other values and headings of the column and row. Consequently, the highlighting should remain while the mouse is moved around.
Here's the finished product. Try it! Click any heading, or cell.
When a heading is clicked, the respective column or row is highlighed or the highlight is removed. The highlight is removed from any previously highlighted column or row. Clicking a cell highlights the intersecting column and row, and clicking the upper left cell clears the highlighting.
Col 1 | Col 2 | Col 3 | Col 4 | Col 5 | Col 6 | Col 7 | Col 8 | Col 9 | Col 10 | Col 11 | Col 12 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
R 1 | 1.1 | 1.2 | 1.3 | 1.4 | 1.5 | 1.6 | 1.7 | 1.8 | 1.9 | 1.10 | 1.11 | 1.12 |
R 2 | 2.1 | 2.2 | 2.3 | 2.4 | 2.5 | 2.6 | 2.7 | 2.8 | 2.9 | 2.10 | 2.11 | 2.12 |
R 3 | 3.1 | 3.2 | 3.3 | 3.4 | 3.5 | 3.6 | 3.7 | 3.8 | 3.9 | 3.10 | 3.11 | 3.12 |
R 4 | 4.1 | 4.2 | 4.3 | 4.4 | 4.5 | 4.6 | 4.7 | 4.8 | 4.9 | 4.10 | 4.11 | 4.12 |
R 5 | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | 5.6 | 5.7 | 5.8 | 5.9 | 5.10 | 5.11 | 5.12 |
R 6 | 6.1 | 6.2 | 6.3 | 6.4 | 6.5 | 6.6 | 6.7 | 6.8 | 6.9 | 6.10 | 6.11 | 6.12 |
R 7 | 7.1 | 7.2 | 7.3 | 7.4 | 7.5 | 7.6 | 7.7 | 7.8 | 7.9 | 7.10 | 7.11 | 7.12 |
R 8 | 8.1 | 8.2 | 8.3 | 8.4 | 8.5 | 8.6 | 8.7 | 8.8 | 8.9 | 8.10 | 8.11 | 8.12 |
R 9 | 9.1 | 9.2 | 9.3 | 9.4 | 9.5 | 9.6 | 9.7 | 9.8 | 9.9 | 9.10 | 9.11 | 9.12 |
R 10 | 10.1 | 10.2 | 10.3 | 10.4 | 10.5 | 10.6 | 10.7 | 10.8 | 10.9 | 10.10 | 10.11 | 10.12 |
The effect is accomplished through a combination of CSS and JavaScript.
.hlt { /* highlighted column or row heading */ background-color: black; color: #ff0; } .hlt-col { /* highlighted cell in column */ background-color: #ff0; color: black; } .hlt td { /* highlighted cell in row */ background-color: #c00; color: #fff; } .hlt .hlt-col { /* cell highlighted by both column and row */ border: 1px solid #f90; /* Makes cell look larger */ background-color: #f90; color: #000; }
The CSS has four different style selectors for highlighting. The non highlighted attributes are inherited or lower priority in the CSS. For example, the style td {background-color: #999;} is overridden by any of the above styles because the selector is less specific than those with a class or a parent child relationship. In fact, the selector .hlt .hlt-col, which expresses a parent child relationship, where a row (TR) has the class hlt and a cell (TD) in that row has the class hlt-col overrides the rules for cell with the same class, hlt-col, and no parent specified in the style selector. That's how the intersected cell gets a different background color than either the row or the column.
Now let's take a look at the JavaScript. As you can see the function mouse_event_handler isn't very different than mouse_event is on the previous page.
function mouse_event_handler (e) { e = e || window.event; var cell = e.srcElement || e.target; var tname = (cell.nodeType == 1) ? cell.tagName.toLowerCase() : ''; while(tname !="td" && tname != "th" && tname != "table"){ cell= cell.parentNode || cell.parentElement; tname = cell.tagName.toLowerCase(); } if (tname == "td" || tname == "th") { var newClass; var cellIdx = _getCellIndex(cell); var row = cell.parentNode || cell.parentElement; var rowIdx = _getRowIndex(row); if (cellIdx == 0 && rowIdx == 0) { _clearHighlight(); } else if (cellIdx == 0) { _setRow(); } else if (rowIdx == 0) { _setCol(); } else { _setRow(); _setCol(); } } function _getTable() { [snipped] } function _clearHighlight() { [snipped] } function _setRow() { [snipped] } function _clearCol() { [snipped] } function _setCol() { [snipped] } function _getCellIndex(cell) { [snipped] } function _getRowIndex(row) { [snipped] } }//eof mouse_event mouse_event_handler.previous = {cellIdx: null, row: null, table: null};
The first unusual thing that stands out is that seven functions are declared inside the event handler, mouse_event_handler. The code is not shown to simplify the explanation. At this point, it is sufficient to know they are there, and that they have access to all variables declared in mouse_event_handler. Also, these functions are not accessible outside of mouse_event; hence, the leading underscore in their names. The underscore is being used as a reminder that the functions are internal methods of an object.
Speaking of objects—all JavaScript functions are objects—the next unusual thing you might notice is the function property, previous, declared after the function declaration. This property is an object with three properties:
A function property was used instead of a global object to encapuslate the values, and protect them, to some degree, from other code. The only way to access the object, previous, is by using the function name and dot notation. The pronoun this doesn't work.
The function, mouse_event_handler, does the the following:
Now let's look at the details of the seven internal methods. These functions were created because it's cleaner to breakout some processes into cohesive single purpose units.
function _getTable() { if (mouse_event_handler.previous.table) return; else { var tbleObj = row.parentNode || row.parentElement; //tbody var tn = tbleObj.tagName.toLowerCase(); while (tn != "table" && tn != "html") { tbleObj = tbleObj.parentNode || tbleObj.parentElement; tn = tbleObj.tagName.toLowerCase(); } mouse_event_handler.previous.table = tbleObj; } }//eof _getTable
_getTable: Because there could be a tbody element or other nodes in between the row and the table element, a loop is used to move up the DOM looking for the table
function _getCellIndex(cell) { var rtrn = cell.cellIndex || 0; if (rtrn == 0) { do{ if (cell.nodeType == 1) rtrn++; cell = cell.previousSibling; } while (cell); --rtrn; } return rtrn; }//eof getCellIndex
_getCellIndex: Getting the cell index is usually as easy as reading the cell's (TD or TH) property cellIndex. Some browsers, however, report an incorrect value of zero for all cells. If the cellIndex is zero, there is a loop to count the cells from the current cell to the first. That count is decremented to make it zero based like the array.
function _getRowIndex(row) { var rtrn = row.rowIndex || 0; if (rtrn == 0) { do{ if (row.nodeType == 1) rtrn++; row = row.previousSibling; } while (row); --rtrn; } return rtrn; }//eof getRowIndex
_getRowIndex: This has the same rational and function as _getCellIndex, but is at the row level.
function _clearRow() { if (mouse_event_handler.previous.row) { mouse_event_handler.previous.row.className = ""; mouse_event_handler.previous.row.cells[0].className = ""; } }//eof clearRow
_clearRow: This does exactly what its name implies. If the object, previous, has a row reference, the row's class name is set to the non highlighted value. Also the first cell, which is a row header (TH) with its own highlight class, has its class name set to the non highlighted value. In this example, all non highlighted class values are empty strings. This lets the CSS cascade and inheritance determine the style.
function _setRow() { _clearRow(); if (tname == 'td' || mouse_event_handler.previous.row != row) { row.className = 'hlt'; row.cells[0].className = 'hlt'; mouse_event_handler.previous.row = row; } else { mouse_event_handler.previous.row = null; } }//eof setRow
_setRow: Highlighting the row starts by removing the highlight from the previously highlighted row by calling _clearRow. If the clicked element was a cell (TD) within the table or a row heading (TH) for a row other than the one currently highlighted, the row is highlight by changing the className to the highlighted style, and the first cell in the row, which is a header, is given a special highlight style. Previous.row is set to the current row. If the clicked row header is the same as the previous row, the row is not highlighted and previous.row is set to null.
function _clearCol() { _getTable(); if (mouse_event_handler.previous.cellIdx != null) { var table = mouse_event_handler.previous.table; var cell = mouse_event_handler.previous.cellIdx; for (var i = 0; i < table.rows.length; i++) { table.rows[i].cells[cell].className = ''; } } }//eof clearCol
_clearCol: This is similar to _clearRow with the exception that a table reference is required. The code loops through the table's rows array changing the cell in a specific position in each row's cells array to remove highlighting.
function _setCol() { _clearCol(); if (tname == 'td' || mouse_event_handler.previous.cellIdx != cellIdx) { mouse_event_handler.previous.table.rows[0].cells[cellIdx].className = 'hlt'; var trs = mouse_event_handler.previous.rows; for (var i = 1; i < trs.length; i++) { trs[i].cells[cellIdx].className = 'hlt-col'; } mouse_event_handler.previous.cellIdx = cellIdx; } else { mouse_event_handler.previous.cellIdx = null; } }//eof setCol
_setCol: After clearing the previous column, the cells in the column clicked are highlighted if a new column or a cell in the table was clicked. Unlike the rows, to highlight a column each cell in that column is highlighed. This is done by stepping through the table's rows array and changing the class of a particular cell in each row's cells array. Then previous.cellIdx is set to the current cellIdx. previous.cellIdx is set to null if the column was not highlighted.
function _clearHighlight() { _clearRow(); _clearCol(); mouse_event_handler.previous.row = null; mouse_event_handler.previous.cellIdx = null; }//eof clearHighlight
_clearHighlight: The last of the seven internal methods simply clears all highlighting and sets the values in previous to null.
You can download the code an use it; however, (disclaimer) the code is for instructional purposes, no claim of fitness for any purpose is made. You are responsible for modifying the code to fit your use.