1// expand/collapse button (expander) is added if height of a cell content
2// exceeds CLIP_HEIGHT px.
3var CLIP_HEIGHT = 135;
4
5// Height in pixels of an expander image.
6var EXPANDER_HEIGHT = 13;
7
8// Path to images for an expander.
9var imgPath = "./images/expandcollapse/";
10
11// array[group][cell] of { 'height', 'expanded' }.
12// group: a number; cells of the same group belong to the same table row.
13// cell: a number; unique index of a cell in a group.
14// height: a number, px; original height of a cell in a table.
15// expanded: boolean; is a cell expanded or collapsed?
16var CellsInfo = [];
17
18// Extracts group and cell indices from an id of the form identifier_group_cell.
19function getCellIdx(id) {
20  var idx = id.substr(id.indexOf("_") + 1).split("_");
21  return { 'group': idx[0], 'cell': idx[1] };
22}
23
24// Returns { 'height', 'expanded' } info for a cell with a given id.
25function getCellInfo(id) {
26  var idx = getCellIdx(id);
27  return CellsInfo[idx.group][idx.cell];
28}
29
30// Initialization, add nodes, collect info.
31function initExpandCollapse() {
32  if (!document.getElementById)
33    return;
34
35  var groupCount = 0;
36
37  // Examine all table rows in the document.
38  var rows = document.body.getElementsByTagName("tr");
39  for (var i=0; i<rows.length; i+=1) {
40
41    var cellCount=0, newGroupCreated = false;
42
43    // Examine all divs in a table row.
44    var divs = rows[i].getElementsByTagName("div");
45    for (var j=0; j<divs.length; j+=1) {
46
47      var expandableDiv = divs[j];
48
49      if (expandableDiv.className.indexOf("expandable") == -1)
50        continue;
51
52      if (expandableDiv.offsetHeight <= CLIP_HEIGHT)
53        continue;
54
55      // We found a div wrapping a cell content whose height exceeds
56      // CLIP_HEIGHT.
57      var originalHeight = expandableDiv.offsetHeight;
58      // Unique postfix for ids for generated nodes for a given cell.
59      var idxStr = "_" + groupCount + "_" + cellCount;
60      // Create an expander and an additional wrapper for a cell content.
61      //
62      //                                --- expandableDiv ----
63      //  --- expandableDiv ---         | ------ data ------ |
64      //  |    cell content   |   ->    | |  cell content  | |
65      //  ---------------------         | ------------------ |
66      //                                | ---- expander ---- |
67      //                                ----------------------
68      var data = document.createElement("div");
69      data.className = "data";
70      data.id = "data" + idxStr;
71      data.innerHTML = expandableDiv.innerHTML;
72      with (data.style) { height = (CLIP_HEIGHT - EXPANDER_HEIGHT) + "px";
73                          overflow = "hidden" }
74
75      var expander = document.createElement("img");
76      with (expander.style) { display = "block"; paddingTop = "5px"; }
77      expander.src = imgPath + "ellipses_light.gif";
78      expander.id = "expander" + idxStr;
79
80      // Add mouse calbacks to expander.
81      expander.onclick = function() {
82        expandCollapse(this.id);
83        // Hack for Opera - onmouseout callback is not invoked when page
84        // content changes dynamically and mouse pointer goes out of an element.
85        this.src = imgPath +
86                   (getCellInfo(this.id).expanded ? "arrows_light.gif"
87                                                  : "ellipses_light.gif");
88      }
89      expander.onmouseover = function() {
90        this.src = imgPath +
91                   (getCellInfo(this.id).expanded ? "arrows_dark.gif"
92                                                  : "ellipses_dark.gif");
93      }
94      expander.onmouseout = function() {
95        this.src = imgPath +
96                   (getCellInfo(this.id).expanded ? "arrows_light.gif"
97                                                  : "ellipses_light.gif");
98      }
99
100      expandableDiv.innerHTML = "";
101      expandableDiv.appendChild(data);
102      expandableDiv.appendChild(expander);
103      expandableDiv.style.height = CLIP_HEIGHT + "px";
104      expandableDiv.id = "cell"+ idxStr;
105
106      // Keep original cell height and its ecpanded/cpllapsed state.
107      if (!newGroupCreated) {
108        CellsInfo[groupCount] = [];
109        newGroupCreated = true;
110      }
111      CellsInfo[groupCount][cellCount] = { 'height' : originalHeight,
112                                           'expanded' : false };
113      cellCount += 1;
114    }
115    groupCount += newGroupCreated ? 1 : 0;
116  }
117}
118
119function isElemTopVisible(elem) {
120  var body = document.body,
121      html = document.documentElement,
122      // Calculate expandableDiv absolute Y coordinate from the top of body.
123      bodyRect = body.getBoundingClientRect(),
124      elemRect = elem.getBoundingClientRect(),
125      elemOffset = Math.floor(elemRect.top - bodyRect.top),
126      // Calculate the absoute Y coordinate of visible area.
127      scrollTop = html.scrollTop || body && body.scrollTop || 0;
128  scrollTop -= html.clientTop; // IE<8
129
130
131  if (elemOffset < scrollTop)
132    return false;
133
134  return true;
135}
136
137// Invoked when an expander is pressed; expand/collapse a cell.
138function expandCollapse(id) {
139  var cellInfo = getCellInfo(id);
140  var idx = getCellIdx(id);
141
142  // New height of a row.
143  var newHeight;
144  // Smart page scrolling may be done after collapse.
145  var mayNeedScroll;
146
147  if (cellInfo.expanded) {
148    // Cell is expanded - collapse the row height to CLIP_HEIGHT.
149    newHeight = CLIP_HEIGHT;
150    mayNeedScroll = true;
151  }
152  else {
153    // Cell is collapsed - expand the row height to the cells original height.
154    newHeight = cellInfo.height;
155    mayNeedScroll = false;
156  }
157
158  // Update all cells (height and expanded/collapsed state) in a row according
159  // to the new height of the row.
160  for (var i = 0; i < CellsInfo[idx.group].length; i++) {
161    var idxStr = "_" + idx.group + "_" + i;
162    var expandableDiv = document.getElementById("cell" + idxStr);
163    expandableDiv.style.height = newHeight + "px";
164    var data = document.getElementById("data" + idxStr);
165    var expander = document.getElementById("expander" + idxStr);
166    var state = CellsInfo[idx.group][i];
167
168    if (state.height > newHeight) {
169      // Cell height exceeds row height - collapse a cell.
170      data.style.height = (newHeight - EXPANDER_HEIGHT) + "px";
171      expander.src = imgPath + "ellipses_light.gif";
172      CellsInfo[idx.group][i].expanded = false;
173    } else {
174      // Cell height is less then or equal to row height - expand a cell.
175      data.style.height = "";
176      expander.src = imgPath + "arrows_light.gif";
177      CellsInfo[idx.group][i].expanded = true;
178    }
179  }
180
181  if (mayNeedScroll) {
182    var idxStr = "_" + idx.group + "_" + idx.cell;
183    var clickedExpandableDiv = document.getElementById("cell" + idxStr);
184    // Scroll page up if a row is collapsed and the rows top is above the
185    // viewport. The amount of scroll is the difference between a new and old
186    // row height.
187    if (!isElemTopVisible(clickedExpandableDiv)) {
188      window.scrollBy(0, newHeight - cellInfo.height);
189    }
190  }
191}
192