1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/domplate",
  5     "firebug/lib/locale",
  6     "firebug/chrome/reps",
  7     "firebug/lib/dom",
  8     "firebug/lib/css",
  9     "firebug/lib/array",
 10 ],
 11 function(Domplate, Locale, FirebugReps, Dom, Css, Arr) {
 12 
 13 // ********************************************************************************************* //
 14 // Constants
 15 
 16 // ********************************************************************************************* //
 17 
 18 with (Domplate) {
 19 FirebugReps.Table = domplate(Firebug.Rep,
 20 {
 21     className: "table",
 22     tableClassName: "dataTable",
 23     tag:
 24         DIV({"class": "dataTableSizer", "tabindex": "-1" },
 25             TABLE({"class": "$tableClassName", cellspacing: 0, cellpadding: 0, width: "100%",
 26                 "role": "grid"},
 27                 THEAD({"class": "dataTableThead", "role": "presentation"},
 28                     TR({"class": "headerRow focusRow dataTableRow subFocusRow", "role": "row",
 29                         onclick: "$onClickHeader"},
 30                         FOR("column", "$object.columns",
 31                             TH({"class": "headerCell a11yFocus", "role": "columnheader",
 32                                 $alphaValue: "$column.alphaValue"},
 33                                 DIV({"class": "headerCellBox"},
 34                                     "$column.label"
 35                                 )
 36                             )
 37                         )
 38                     )
 39                 ),
 40                 TBODY({"class": "dataTableTbody", "role": "presentation"},
 41                     FOR("row", "$object.data|getRows",
 42                         TR({"class": "focusRow dataTableRow subFocusRow", "role": "row"},
 43                             FOR("column", "$row|getColumns",
 44                                 TD({"class": "a11yFocus dataTableCell", "role": "gridcell"},
 45                                     TAG("$column|getValueTag", {object: "$column"})
 46                                 )
 47                             )
 48                         )
 49                     )
 50                 )
 51             )
 52         ),
 53 
 54     getValueTag: function(object)
 55     {
 56         var rep = Firebug.getRep(object);
 57         return rep.shortTag || rep.tag;
 58     },
 59 
 60     getRows: function(data)
 61     {
 62         var props = this.getProps(data);
 63         if (!props.length)
 64             return [];
 65         return props;
 66     },
 67 
 68     getColumns: function(row)
 69     {
 70         if (typeof(row) != "object")
 71             return [row];
 72 
 73         var cols = [];
 74         for (var i=0; i<this.columns.length; i++)
 75         {
 76             var prop = this.columns[i].property;
 77 
 78             // Object property is not set for this column, so display entire
 79             // row-value in the cell. This can happen in cases where a generic
 80             // object is logged using table layout. In such case there is one
 81             // column (no property associated) and each row represents a member
 82             // of the object.
 83             if (typeof prop == "undefined")
 84             {
 85                 value = row;
 86             }
 87             else if (typeof row[prop] == "undefined")
 88             {
 89                 var props = (typeof(prop) == "string") ? prop.split(".") : [prop];
 90 
 91                 var value = row;
 92                 for (var p in props)
 93                     value = (value && value[props[p]]) || undefined;
 94             }
 95             else
 96             {
 97                 value = row[prop];
 98             }
 99 
100             cols.push(value);
101         }
102         return cols;
103     },
104 
105     getProps: function(obj)
106     {
107         if (typeof(obj) != "object")
108             return [obj];
109 
110         if (FBTrace.DBG_CONSOLE)
111             FBTrace.sysout("FirebugReps.Table.getProps", {obj: obj, arr: Arr.cloneArray(obj)});
112 
113         if (obj.length)
114             return Arr.cloneArray(obj);
115 
116         var arr = [];
117         for (var p in obj)
118         {
119             var value = obj[p];
120             if (this.domFilter(value, p))
121                 arr.push(value);
122         }
123 
124         return arr;
125     },
126 
127     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
128     // Sorting
129 
130     onClickHeader: function(event)
131     {
132         var table = Dom.getAncestorByClass(event.target, "dataTable");
133         var header = Dom.getAncestorByClass(event.target, "headerCell");
134         if (!header)
135             return;
136 
137         var numerical = !Css.hasClass(header, "alphaValue");
138 
139         var colIndex = 0;
140         for (header = header.previousSibling; header; header = header.previousSibling)
141             ++colIndex;
142 
143         this.sort(table, colIndex, numerical);
144     },
145 
146     sort: function(table, colIndex, numerical)
147     {
148         var tbody = Dom.getChildByClass(table, "dataTableTbody");
149         var thead = Dom.getChildByClass(table, "dataTableThead");
150 
151         var values = [];
152         for (var row = tbody.childNodes[0]; row; row = row.nextSibling)
153         {
154             var cell = row.childNodes[colIndex];
155             var value = numerical ? parseFloat(cell.textContent) : cell.textContent;
156             values.push({row: row, value: value});
157         }
158 
159         values.sort(function(a, b) { return a.value < b.value ? -1 : 1; });
160 
161         var headerRow = thead.firstChild;
162         var headerSorted = Dom.getChildByClass(headerRow, "headerSorted");
163         Css.removeClass(headerSorted, "headerSorted");
164         if (headerSorted)
165             headerSorted.removeAttribute('aria-sort');
166 
167         var header = headerRow.childNodes[colIndex];
168         Css.setClass(header, "headerSorted");
169 
170         if (!header.sorted || header.sorted == 1)
171         {
172             Css.removeClass(header, "sortedDescending");
173             Css.setClass(header, "sortedAscending");
174             header.setAttribute("aria-sort", "ascending");
175 
176             header.sorted = -1;
177 
178             for (var i = 0; i < values.length; ++i)
179                 tbody.appendChild(values[i].row);
180         }
181         else
182         {
183             Css.removeClass(header, "sortedAscending");
184             Css.setClass(header, "sortedDescending");
185             header.setAttribute("aria-sort", "descending")
186 
187             header.sorted = 1;
188 
189             for (var i = values.length-1; i >= 0; --i)
190                 tbody.appendChild(values[i].row);
191         }
192     },
193 
194     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
195     // Console logging
196 
197     log: function(data, cols, context, object)
198     {
199         // No arguments passed into console.table method, bail out for now,
200         // but some error message could be displayed in the future.
201         if (!data)
202             return;
203 
204         var columns = this.computeColumns(data, cols);
205 
206         // Don't limit strings in the table. It should be mostly ok. In case of
207         // complaints we need an option.
208         var prevValue = Firebug.stringCropLength;
209         Firebug.stringCropLength = -1;
210 
211         try
212         {
213             this.columns = columns;
214 
215             var object = object || {};
216             object.data = data;
217             object.columns = columns;
218 
219             var row = Firebug.Console.log(object, context, "table", this, true);
220 
221             // Set vertical height for scroll bar.
222             var tBody = row.querySelector(".dataTableTbody");
223             var maxHeight = Firebug.tabularLogMaxHeight;
224             if (maxHeight > 0 && tBody.clientHeight > maxHeight)
225                 tBody.style.height = maxHeight + "px";
226         }
227         catch (err)
228         {
229             if (FBTrace.DBG_CONSOLE)
230                 FBTrace.sysout("consoleInjector.table; EXCEPTION " + err, err);
231         }
232         finally
233         {
234             Firebug.stringCropLength = prevValue;
235             delete this.columns;
236         }
237     },
238 
239     computeColumns: function(data, cols)
240     {
241         // Get header info from passed argument (can be null).
242         var columns = [];
243         for (var i=0; cols && i<cols.length; i++)
244         {
245             var col = cols[i];
246             var prop = (typeof(col.property) != "undefined") ? col.property : col;
247             var label = (typeof(col.label) != "undefined") ? col.label : prop;
248 
249             columns.push({
250                 property: prop,
251                 label: label,
252                 alphaValue: true
253             });
254         }
255 
256         if (FBTrace.DBG_CONSOLE)
257             FBTrace.sysout("consoleInjector.table; columns", columns);
258 
259         // Generate header info from the data dynamically.
260         if (!columns.length)
261             columns = this.getHeaderColumns(data);
262 
263         return columns;
264     },
265 
266     /**
267      * Analyse data and return dynamically created list of columns.
268      * @param {Object} data
269      */
270     getHeaderColumns: function(data)
271     {
272         // Get the first row in the object.
273         var firstRow;
274         for (var p in data)
275         {
276             firstRow = data[p];
277             break;
278         }
279 
280         if (typeof(firstRow) != "object")
281             return [{label: Locale.$STR("firebug.reps.table.ObjectProperties")}];
282 
283         // Put together a column property, label and type (type for default sorting logic).
284         var header = [];
285         for (var p in firstRow)
286         {
287             var value = firstRow[p];
288             if (!this.domFilter(value, p))
289                 continue;
290 
291             header.push({
292                 property: p,
293                 label: p,
294                 alphaValue: (typeof(value) != "number")
295             });
296         }
297 
298         return header;
299     },
300 
301     /**
302      * Filtering based on options set in the DOM panel.
303      * @param {Object} value - a property value under inspection.
304      * @param {String} name - name of the property.
305      * @returns true if the value should be displayed, otherwise false.
306      */
307     domFilter: function(object, name)
308     {
309         var domMembers = Dom.getDOMMembers(object, name);
310 
311         if (typeof(object) == "function")
312         {
313             if (Dom.isDOMMember(object, name) && !Firebug.showDOMFuncs)
314                 return false;
315             else if (!Firebug.showUserFuncs)
316                 return false;
317         }
318         else
319         {
320             if (Dom.isDOMMember(object, name) && !Firebug.showDOMProps)
321                 return false;
322             else if (Dom.isDOMConstant(object, name) && !Firebug.showDOMConstants)
323                 return false;
324             else if (!Firebug.showUserProps)
325                 return false;
326         }
327 
328         return true;
329     }
330 })};
331 
332 // ********************************************************************************************* //
333 // Registration
334 
335 return FirebugReps.Table;
336 
337 // ********************************************************************************************* //
338 });
339