expandcollapse.js 6.78 KB
// expand/collapse button (expander) is added if height of a cell content 
// exceeds CLIP_HEIGHT px.
var CLIP_HEIGHT = 135;

// Height in pixels of an expander image.
var EXPANDER_HEIGHT = 13;

// Path to images for an expander.
var imgPath = "./images/expandcollapse/";

// array[group][cell] of { 'height', 'expanded' }.
// group: a number; cells of the same group belong to the same table row.
// cell: a number; unique index of a cell in a group.
// height: a number, px; original height of a cell in a table.
// expanded: boolean; is a cell expanded or collapsed?
var CellsInfo = [];

// Extracts group and cell indices from an id of the form identifier_group_cell.
function getCellIdx(id) {
  var idx = id.substr(id.indexOf("_") + 1).split("_");
  return { 'group': idx[0], 'cell': idx[1] };
}

// Returns { 'height', 'expanded' } info for a cell with a given id.
function getCellInfo(id) { 
  var idx = getCellIdx(id); 
  return CellsInfo[idx.group][idx.cell]; 
}

// Initialization, add nodes, collect info.
function initExpandCollapse() {
  if (!document.getElementById)
    return;

  var groupCount = 0;

  // Examine all table rows in the document.
  var rows = document.body.getElementsByTagName("tr");
  for (var i=0; i<rows.length; i+=1) {

    var cellCount=0, newGroupCreated = false;

    // Examine all divs in a table row.
    var divs = rows[i].getElementsByTagName("div");
    for (var j=0; j<divs.length; j+=1) {

      var expandableDiv = divs[j];

      if (expandableDiv.className.indexOf("expandable") == -1)
        continue;

      if (expandableDiv.offsetHeight <= CLIP_HEIGHT)
        continue;

      // We found a div wrapping a cell content whose height exceeds 
      // CLIP_HEIGHT.
      var originalHeight = expandableDiv.offsetHeight;
      // Unique postfix for ids for generated nodes for a given cell.
      var idxStr = "_" + groupCount + "_" + cellCount;
      // Create an expander and an additional wrapper for a cell content.
      //
      //                                --- expandableDiv ----
      //  --- expandableDiv ---         | ------ data ------ |
      //  |    cell content   |   ->    | |  cell content  | | 
      //  ---------------------         | ------------------ |
      //                                | ---- expander ---- |
      //                                ----------------------
      var data = document.createElement("div");
      data.className = "data";
      data.id = "data" + idxStr;
      data.innerHTML = expandableDiv.innerHTML;
      with (data.style) { height = (CLIP_HEIGHT - EXPANDER_HEIGHT) + "px";
                          overflow = "hidden" }

      var expander = document.createElement("img");
      with (expander.style) { display = "block"; paddingTop = "5px"; }
      expander.src = imgPath + "ellipses_light.gif";
      expander.id = "expander" + idxStr;

      // Add mouse calbacks to expander.
      expander.onclick = function() {
        expandCollapse(this.id);
        // Hack for Opera - onmouseout callback is not invoked when page 
        // content changes dynamically and mouse pointer goes out of an element.
        this.src = imgPath + 
                   (getCellInfo(this.id).expanded ? "arrows_light.gif"
                                                  : "ellipses_light.gif");
      }
      expander.onmouseover = function() { 
        this.src = imgPath + 
                   (getCellInfo(this.id).expanded ? "arrows_dark.gif"
                                                  : "ellipses_dark.gif");
      }
      expander.onmouseout = function() { 
        this.src = imgPath + 
                   (getCellInfo(this.id).expanded ? "arrows_light.gif"
                                                  : "ellipses_light.gif");
      }

      expandableDiv.innerHTML = "";
      expandableDiv.appendChild(data);
      expandableDiv.appendChild(expander);
      expandableDiv.style.height = CLIP_HEIGHT + "px";
      expandableDiv.id = "cell"+ idxStr;

      // Keep original cell height and its ecpanded/cpllapsed state.
      if (!newGroupCreated) {
        CellsInfo[groupCount] = [];
        newGroupCreated = true;
      }
      CellsInfo[groupCount][cellCount] = { 'height' : originalHeight,
                                           'expanded' : false };
      cellCount += 1;
    }
    groupCount += newGroupCreated ? 1 : 0;
  }
}

function isElemTopVisible(elem) {
  var body = document.body,
      html = document.documentElement,
      // Calculate expandableDiv absolute Y coordinate from the top of body.
      bodyRect = body.getBoundingClientRect(),
      elemRect = elem.getBoundingClientRect(),
      elemOffset = Math.floor(elemRect.top - bodyRect.top),
      // Calculate the absoute Y coordinate of visible area.
      scrollTop = html.scrollTop || body && body.scrollTop || 0;
  scrollTop -= html.clientTop; // IE<8

  
  if (elemOffset < scrollTop)
    return false;

  return true;
}

// Invoked when an expander is pressed; expand/collapse a cell.
function expandCollapse(id) {
  var cellInfo = getCellInfo(id);
  var idx = getCellIdx(id);

  // New height of a row.
  var newHeight;
  // Smart page scrolling may be done after collapse.
  var mayNeedScroll;

  if (cellInfo.expanded) {
    // Cell is expanded - collapse the row height to CLIP_HEIGHT.
    newHeight = CLIP_HEIGHT;
    mayNeedScroll = true;
  }
  else {
    // Cell is collapsed - expand the row height to the cells original height.
    newHeight = cellInfo.height;
    mayNeedScroll = false;
  }

  // Update all cells (height and expanded/collapsed state) in a row according 
  // to the new height of the row.
  for (var i = 0; i < CellsInfo[idx.group].length; i++) {
    var idxStr = "_" + idx.group + "_" + i;
    var expandableDiv = document.getElementById("cell" + idxStr);
    expandableDiv.style.height = newHeight + "px";
    var data = document.getElementById("data" + idxStr);
    var expander = document.getElementById("expander" + idxStr);
    var state = CellsInfo[idx.group][i];

    if (state.height > newHeight) {
      // Cell height exceeds row height - collapse a cell.
      data.style.height = (newHeight - EXPANDER_HEIGHT) + "px";
      expander.src = imgPath + "ellipses_light.gif";
      CellsInfo[idx.group][i].expanded = false;
    } else {
      // Cell height is less then or equal to row height - expand a cell.
      data.style.height = "";
      expander.src = imgPath + "arrows_light.gif";
      CellsInfo[idx.group][i].expanded = true;
    }
  }

  if (mayNeedScroll) {
    var idxStr = "_" + idx.group + "_" + idx.cell;
    var clickedExpandableDiv = document.getElementById("cell" + idxStr);
    // Scroll page up if a row is collapsed and the rows top is above the 
    // viewport. The amount of scroll is the difference between a new and old 
    // row height.
    if (!isElemTopVisible(clickedExpandableDiv)) {
      window.scrollBy(0, newHeight - cellInfo.height);
    }
  }
}