4

I am trying to determine the widest content in HTML table cells of fixed width. Currently I am using an external sizer DIV:

 <div id="xdivSizer" style="position:absolute;display:inline;visibility:hidden">
 </div>

in this code

var iColWidth = 0;

for (var Y = 0; Y < oDataTable.rows.length; Y++) {
    oDivSizer.innerHTML = oDataTable.rows[Y].cells[0].innerHTML;
    iColWidth = Math.max(oDivSizer.offsetWidth, iColWidth)
}

It works, but extremally slow, even when the table has only 100 rows or so. It looks like main offender is calculating of offsetWidth. Is there a more efficient way to achieve this?

I am doing this because I need to resize that table column to the size of the widest data. If there're other better way, that would be great as well.

(More precisely I am trying to implement "column autosize" feature in Infragistics WebHierarchicalDataGrid control for ASP.NET - columns need to take the widest width Max(Header Width, Data Width) while maintaining fixed header/pager positions).

Thanks!

Yuriy Galanter
  • 38,833
  • 15
  • 69
  • 136
  • does the approach here meet your needs: http://www.infragistics.com/community/forums/t/60115.aspx – alhalama Dec 09 '12 at 01:08
  • Hi and thanks for the reply. But this is exactly same approach I am using (A DIV is assigned HTML content of cell and then offsetWidth property is used). The only difference - Infragistics approach does it for only 1st cell in the column, so if some cell down the road is wider then the 1st - the sizing won't work. But the idea is the same, which while works is really slow. – Yuriy Galanter Dec 10 '12 at 15:28
  • If you set the DefaultColumnWidth and the Width of each column to an empty string or Unit.Empty in code then the column will size to the contents. Once you do this you will only need to check the first cell in the column against the header and that should address your performance issue. – alhalama Dec 11 '12 at 00:23
  • Nope that doesn't work. If I don't set column width initially, grid simple sizes them equally along the grid width – Yuriy Galanter Dec 11 '12 at 21:05
  • It's not clear for me what you want to do. Do you want to do automatic width on column, where width is based on the length of a content in the column ? – Zaffy Dec 13 '12 at 14:04
  • In effect yes, but this is not just a plain HTML table, it's a server-rendered control which has features such as fixed headers and footers (while data grid is scrolled vertically) headers and footers remain fixed in place etc. So the only working way I found so far is preset column width in advance and then resize them based on data size. But I'd appreciate if you could show a better way. – Yuriy Galanter Dec 13 '12 at 14:09
  • http://blog.mastykarz.nl/measuring-the-length-of-a-string-in-pixels-using-javascript/ This method uses a tag. Not sure if it would perform better. http://daipratt.co.uk/calculate-text-width-and-height-with-javascript/ This one uses a

    tag, but it looks a bit sketchy to me. You might also try clientWidth instead of offsetWidth, though that includes the padding and I don't know if you want that, but you never know, it may be more performant to do that and then subtract out the padding (probably not).

    – Pete Dec 13 '12 at 17:24
  • @Trekstuff Still not clear... do you want to avoid line-breaks if the cell width is not long enough for the text? – Zaffy Dec 14 '12 at 00:13
  • Yes, the text should not wrap unless actual formatting (
    ,

    etc.) is encountered

    – Yuriy Galanter Dec 14 '12 at 14:07
  • @YuriyGalanter I have taken another look at this again and posted a solution that only checks the first row and the header after allowing the columns to auto size to their content in the data portion of the grid. If you are still looking for a solution to this you could test this approach. – alhalama Feb 20 '13 at 19:21

2 Answers2

2

Rewriting the DIV contents will trigger copious redraws, applications of your CSS, DOM manipulations etc..

Try putting all of the cells from the column in the DIV all at once, separated by line breaks -- or each in it's own sub-DIV. The width of the resulting DIV should be that of the longest cell.

var html = [];
for (var Y = 0; Y < oDataTable.rows.length; Y++) {
    html.push(oDataTable.rows[Y].cells[0].innerHTML);
}

oDivSizer.innerHTML = html.join("<br />");
var iColWidth = Math.max(oDivSizer.offsetWidth, 1);

Another possibility is a simple heuristic based on each cell's innerText.

var iColWidth = 0;
for (var Y = 0; Y < oDataTable.rows.length; Y++) {
    var w = Math.floor(oDataTable.rows[Y].cells[0].innerText.length * 11.5);
    iColWidth = Math.max(w, iColWidth);
}

And yet another possibility, as indicated in my last comment, is to redraw only the TD in the column that consumes the greatest area.

var a = 0;
var n = null;
for (var Y = 0; Y < oDataTable.rows.length; Y++) {
    var _a = oDataTable.rows[Y].cells[0].offsetHeight * oDataTable.rows[Y].cells[0].offsetWidth;
    if (Number(_a) > a) {
      a = _a;
      n = oDataTable.rows[Y].cells[0];
    }
}
oDivSizer.innerHTML = n.innerHTML;
var iColWidth = Math.max(oDivSizer.offsetWidth, 1);

Or, taking it a step further, divide the calculated area of each cell by the number of BR's found to account for "intentionally" tall cells.

var a = 0;
var n = null;
for (var Y = 0; Y < oDataTable.rows.length; Y++) {
    var _a = oDataTable.rows[Y].cells[0].offsetHeight * oDataTable.rows[Y].cells[0].offsetWidth;
    var brs = oDataTable.rows[Y].cells[0].getElementsByTagName('br');
    _a = Number(_a) / brs.length;
    if (_a > a) {
      a = _a;
      n = oDataTable.rows[Y].cells[0];
    }
}
oDivSizer.innerHTML = n.innerHTML;
var iColWidth = Math.max(oDivSizer.offsetWidth, 1);

But, as the complexity of the fuzzy approaches grows and the result becomes more accurate, you can expect performance to decline.

As a side note, if this auto-sizing is being applied as the result of a user interaction, I'd suggest performing the sizing calculations / processes immediately after the table is available to be analyzed, rather than waiting for the user to initiate the resize.

svidgen
  • 13,744
  • 4
  • 33
  • 58
  • Thanks for the suggestions. This is noticeable faster, but there's still visible delay. Also I believe that when amount of HTML is quite large it may begin to scroll the page - even though DIV's visibility is hidden. – Yuriy Galanter Dec 13 '12 at 21:18
  • Yeah. I don't imagine you'll find a mechanism that's "instantaneous." You either need some sort of heuristic like `cell[i].innerText.length * 14` or you need to let the browser actually redraw the content. But, a heuristic based on each cell's `innerText` *is* another possibility ... I'll add that to my answer, since no other answers have been given. – svidgen Dec 13 '12 at 22:49
  • Very cool approach. I could use it for table headers since their style is pretty much set. Unfortunately, table cells can potentially have HTML content with it's own styles, fonts, line breaks etc. – Yuriy Galanter Dec 13 '12 at 22:58
  • Sure. I suspect a heuristic is the only option for the sort of efficiency you desire. That said, the innerText-based heuristic in my answer is *very* simple. You could potentially make it better by using innerHTML instead, splitting on `/
    )?/`, counting the number of times `` appears, etc., and assigning each tag some weight. It won't be perfect, of course -- it's a heuristic! But, that's the point really. You need something that can estimate the width without determining it precisely, because that requires the browser to actually draw it. Good luck!
    – svidgen Dec 13 '12 at 23:21
  • Another option would be to iterate through the cells, find the one `innerHeight * innerWidth` is the largest, and only redraw *that one* in your hidden `div`. Another heuristic -- more accurate than counting characters if you're cells contain a lot of markup. – svidgen Dec 13 '12 at 23:24
  • Shoot. Heuristic Height*Width sounded as such a brilliant idea, I was certain it would work, Unfortunately cell height can be affected by its neighbors in the same row (cell itself can have very little data but if it's neighbor is loaded with text - cell height will also be affected). But you've given me so much to work with, ideas I have not even considered, I am accepting the answer. Thanks! – Yuriy Galanter Dec 14 '12 at 15:19
  • Ah, touche. Well, if you don't find a workable solution soon, feel free to post another comment here. I'd be willing to give it some more thought. (Perhaps, instead of calculating the w*h of the TD itself, sum up the areas of each non-blank child node of the TD?) – svidgen Dec 14 '12 at 15:27
0

By default the WebDataGrid will size the content area of the grid to its contents when there is no width specified on the column and on the grid itself. The headers then get sized to the data portion. This will work well whether the grid is in a container or outside of a container.

Since the data portion of the grid is sized correctly, all we need to do is size the columns to what is larger, the width of the first cell in the grid or a measurement for displaying the full header. The following JavaScript will do this:

function resizeGrid(grid) {
    var textWidthChecker = document.getElementById('textWidthChecker');
    var columns = grid.get_columns();
    var widths = new Array(columns.get_length());
    var row = grid.get_rows().get_row(0);
    var newGridWidth = 0;
    for (var colIdx = 0; colIdx < columns.get_length(); colIdx++) {
        var col = columns.get_column(colIdx);
        textWidthChecker.innerHTML = col.get_headerText();
        var cellElement = row.get_cell(colIdx)._element;
        if (!col.get_hidden()) {
            if (textWidthChecker.offsetWidth > cellElement.offsetWidth) {
                // 8 for padding in header.
                newGridWidth += textWidthChecker.offsetWidth + 8;
                widths[colIdx] = textWidthChecker.offsetWidth + 8;
            } else {
                widths[colIdx] = cellElement.offsetWidth;
                newGridWidth += cellElement.offsetWidth;
            }
        }
    }
    newGridWidth += columns.get_length() * 16; // added for padding on cells
    var gridElement = grid.get_element();            
    gridElement.style.width = newGridWidth + 'px';
    for (var colIdx = 0; colIdx < columns.get_length(); colIdx++) {
        var col = columns.get_column(colIdx);
        col.set_width(widths[colIdx] + 'px');
    }
}

The above requires that there be a span with id “textWidthChecker” for this to work using the same CSS classes as the header of the grid for the font:

<div style="height: 0px;overflow: hidden;" class="igg_HeaderCaption ig_Control">
    <span id="textWidthChecker"></span>
</div>

This approach works well when the resizeGrid function is called from the client side Initialize method as long as the grid is within a container that is wide enough for the grid when the headers are resized.

If the grid is in a container that is smaller this will fail because the WebDataGrid sets its width internally and this affects the sizing of the columns. To get around this the resizeGrid function needs to be called earlier though there isn’t any event for this.

Specifically the resizeGrid needs to be called during the initialize function of the grid between the calls to _initializeObjects and _adjustGridLayout. To do this you can modify the prototype of the the WebDataGrid to replace the _adjustGridLayout with another one that calls resizeGrid and calls the original _adjustGridLayout:

var adjustGridLayoutFunction = $IG.WebDataGrid.prototype._adjustGridLayout;
$IG.WebDataGrid.prototype._originalAdjustGridLayout = adjustGridLayoutFunction;
$IG.WebDataGrid.prototype._adjustGridLayout = function () {
    resizeGrid(this);
    this._originalAdjustGridLayout();
}; 

Note that modifying the prototype affects every grid on the page so if you wanted to control which grid this applied to you would need to add conditional logic in the new _adjustGridLayout function. For example:

if (this.get_id() == "WebDataGrid1")
    resizeGrid(this);

To prevent wrapping of the text within the grid you also need to use css for the ItemStyle:

.noWrapCellStyle {
    white-space: nowrap;
}

For this to work the desired width should be set on the container of the grid and the grids width should remain empty:

<div style="width:300px;overflow: auto;">
    <ig:WebDataGrid ID="WebDataGrid1" runat="server" Height="450px" AutoGenerateColumns="True" ItemCssClass="noWrapCellStyle">
    </ig:WebDataGrid>
</div>

If you are using row selectors, then the calculations will need to be updated in the logic that I am providing to account for the row selector.

alhalama
  • 3,218
  • 1
  • 15
  • 21
  • Thanks for a very extensive reply, I am looking into ways of incorporating it. But a problem remains with not specifying overall grid width. As you mentioned it means the grid has to scroll within an outer container and that means grid elements that are usually static ( grid pager during horizontal scroll; grid headers during vertical scroll) will also scroll as well. Is there a way around this? – Yuriy Galanter Feb 22 '13 at 19:31
  • For the headers, this shouldn't be an issue if you don't specify a height for the container as it will size to the contents. For the pager, this will be an issue as it will be scrolled out of view to the right at times and I would need to look at this again though you might be able to hide the grids pager and implement one external to the grid. – alhalama Feb 22 '13 at 22:25
  • @alhalama: I have a similar issue as Yuriy - when the grid displays 300 rows -two scrollbars appear (one from grid + one from div). Have you find any solution to it? (For me paging is not an option but I can use virtual scrolling) – mas_oz2k1 Jun 04 '13 at 07:20
  • The div containing the grid doesn't need to have a height set and you can set a height on the grid. – alhalama Jun 04 '13 at 15:59
  • @alhalama: Actually it does if it is part of a page, but if I set height on the grid only and not in the div as per your comment, the vertical scrollbar will appear on the grid and both scrollbars in the div for 300 rows x 15 columns scenario. I tried setting the div height to grid height + 20 px to get rid of the div vertical horizontal scrollbar, but then user will need to use div horizontal scrollbar to reach the vertical scrollbar on the grid - the UX will be different to a typical windows app in which both scrollbars are visible. Please advise. – mas_oz2k1 Jun 05 '13 at 06:06
  • You would need to add a scroll bar external to the grid and remove the one that the grid provides. Then you would need to manually set the scroll position of the grid. I would recommend that you submit a feature request if you haven't already done so: https://www.infragistics.com/my-account/feature-request/ – alhalama Jun 05 '13 at 12:19