fnFakeRowspan (server-side processing only)

fnFakeRowspan (server-side processing only)

vexvex Posts: 30Questions: 0Answers: 0
edited February 2010 in Plug-ins
I finally was assigned a case where I needed rowspan support, but since I didn't want to loose DataTables support there was just one option. Fake the rowspan support! Before you start cheering, this _only_ works with server-side processing of tables and is limited to just one column.

This plugin hooks in to the fnDrawCallback and remove cells and add rowspan to others, thus the data is impossible for DataTables to sort. When using server-side processing the data is always sorted by the server and then returns new data.

Sadly I don't have any time to handle support issues, this works for me and I'm releasing it as-is. Should I discover bugs I will fix them and then post updated versions here.

Support for several columns should be pretty easy to add for those who need it, I do not and haven't put any time to it.

[code]$.fn.dataTableExt.oApi.fnFakeRowspan = function ( oSettings, iColumn ) {
/*
* Type: Plugin for DataTables (http://datatables.net) JQuery plugin.
* Name: dataTableExt.oApi.fnFakeRowspan
* Requires: DataTables 1.6.0+
* Version: 1.0.0
* Description: Creates rowspan cells in a column when there are two or more
* cells in a row with the same content. It only works for
* server-side processing as cells are removed and can't be
* sorted by DataTables.
*
* Inputs: object:oSettings - dataTables settings object
* integer:iColumn - the column to fake rowspans in
* Returns: JQuery
* Usage: $('#example').dataTable().fnFakeRowspan(3);
*
* Author: Fredrik Wendel
* Created: 2010-02-10
* Language: Javascript
* License: GPL v2 or BSD 3 point style
*/

/* Fail silently on missing/errorenous parameter data. */
if (isNaN(iColumn)) {
return false;
}

if (iColumn < 0 || iColumn > oSettings.aoColumns.length-1) {
alert ('Invalid column number choosen, must be between 0 and ' + (oSettings.aoColumns.length-1));
return false;
}

var oSettings = oSettings,
iColumn = iColumn;

oSettings.aoDrawCallback.push({ "fn": fakeRowspan, "sName": "fnFakeRowspan" });

function fakeRowspan () {
var firstOccurance = null,
value = null,
rowspan = 0;
jQuery.each(oSettings.aoData, function (i, oData) {
var val = oData._aData[iColumn],
cell = oData.nTr.childNodes[iColumn];
/* Reset values on new cell data. */
if (val != value) {
value = val;
firstOccurance = cell;
rowspan = 0;
}

if (val == value) {
rowspan++;
}

if (firstOccurance !== null && val == value && rowspan > 1) {
oData.nTr.removeChild(cell);
firstOccurance.rowSpan = rowspan;
}
});
}

return this;
}[/code]

Code only tested with DataTables 1.6.1 (should work with 1.6.0 though) and Firefox 3.6, Opera 10.10, Chrome 4.0.249.78, Safari 4.0.4, Internet Explorer 8.0.

I hope that sharing this 2-3 hack helps someone, and if you appreciate it, please consider donating to Allan for future development of DataTables.

Replies

  • allanallan Posts: 63,680Questions: 1Answers: 10,498 Site admin
    Hi vex,

    Very cool - in fact, how appropriate your slick plug-in function is thread number 1337 :-)

    I'll have a play around with this in a bit, when I've got a bit of free time - but it looks great. Thanks very muhc indeed for sharing it with us!

    Regards,
    Allan
  • vexvex Posts: 30Questions: 0Answers: 0
    Minor update:
    1.0.1 - add bCaseSensitive option for the ability do to case-insensitive comparisons.
    [code]$.fn.dataTableExt.oApi.fnFakeRowspan = function ( oSettings, iColumn, bCaseSensitive ) {
    /*
    * Type: Plugin for DataTables (http://datatables.net) JQuery plugin.
    * Name: dataTableExt.oApi.fnFakeRowspan
    * Requires: DataTables 1.6.0+
    * Version: 1.0.1
    * Description: Creates rowspan cells in a column when there are two or more
    * cells in a row with the same content. It only works for
    * server-side processing as cells are removed and can't be
    * sorted by DataTables.
    *
    * Inputs: object:oSettings - dataTables settings object
    * integer:iColumn - the column to fake rowspans in
    * boolean:bCaseSensitive - whether the comparison is case-sensitive or not (default: true)
    * Returns: JQuery
    * Usage: $('#example').dataTable().fnFakeRowspan(3);
    * $('#example').dataTable().fnFakeRowspan(3, false);
    *
    * Author: Fredrik Wendel
    * Created: 2010-02-10
    * Language: Javascript
    * License: GPL v2 or BSD 3 point style
    */

    /* Fail silently on missing/errorenous parameter data. */
    if (isNaN(iColumn)) {
    return false;
    }

    if (iColumn < 0 || iColumn > oSettings.aoColumns.length-1) {
    alert ('Invalid column number choosen, must be between 0 and ' + (oSettings.aoColumns.length-1));
    return false;
    }

    var oSettings = oSettings,
    iColumn = iColumn,
    bCaseSensitive = (typeof(bCaseSensitive) != 'boolean' ? true : bCaseSensitive);

    oSettings.aoDrawCallback.push({ "fn": fakeRowspan, "sName": "fnFakeRowspan" });

    function fakeRowspan () {
    var firstOccurance = null,
    value = null,
    rowspan = 0;
    jQuery.each(oSettings.aoData, function (i, oData) {
    var val = oData._aData[iColumn],
    cell = oData.nTr.childNodes[iColumn];
    /* Use lowercase comparison if not case-sensitive. */
    if (!bCaseSensitive) {
    val = val.toLowerCase();
    }
    /* Reset values on new cell data. */
    if (val != value) {
    value = val;
    firstOccurance = cell;
    rowspan = 0;
    }

    if (val == value) {
    rowspan++;
    }

    if (firstOccurance !== null && val == value && rowspan > 1) {
    oData.nTr.removeChild(cell);
    firstOccurance.rowSpan = rowspan;
    }
    });
    }

    return this;
    }[/code]
  • allanallan Posts: 63,680Questions: 1Answers: 10,498 Site admin
    Nice :-) Thanks for the update. I've just had a little play around with it and it's looking really good! I know folks will really like this.

    One little issue, which would be nice to resolve before I post it on the plug-ins page, if I put into this example page: http://datatables.net/examples/data_sources/server_side.html - then when I click 'next' it stalls. No JS error - it just stops. I can do a bit of sorting and stuff and that all works well - so I think one of the cells must have something in it which is causing a problem. Might you be able to have a little look at this?

    Regards,
    Allan
  • vexvex Posts: 30Questions: 0Answers: 0
    I'll have a look at it and see if I can find why it dies.
  • vexvex Posts: 30Questions: 0Answers: 0
    I've just had a look and it just works fine for me, tried it on all columns (though instead of a real database I just used an ugly PHP hack):
    [code]<?php
    $response = array(
    '0' => '{"sEcho": __ECHO__, "iTotalRecords": 57, "iTotalDisplayRecords": 57, "aaData": [ ["Gecko","Firefox 1.0","Win 98+ / OSX.2+","1.7","A"],["Gecko","Firefox 1.5","Win 98+ / OSX.2+","1.8","A"],["Gecko","Firefox 2.0","Win 98+ / OSX.2+","1.8","A"],["Gecko","Firefox 3.0","Win 2k+ / OSX.3+","1.9","A"],["Gecko","Camino 1.0","OSX.2+","1.8","A"],["Gecko","Camino 1.5","OSX.3+","1.8","A"],["Gecko","Netscape 7.2","Win 95+ / Mac OS 8.6-9.2","1.7","A"],["Gecko","Netscape Browser 8","Win 98SE+","1.7","A"],["Gecko","Netscape Navigator 9","Win 98+ / OSX.2+","1.8","A"],["Gecko","Mozilla 1.0","Win 95+ / OSX.1+","1","A"]] }',
    '10' => '{"sEcho": __ECHO__, "iTotalRecords": 57, "iTotalDisplayRecords": 57, "aaData": [ ["Gecko","Mozilla 1.1","Win 95+ / OSX.1+","1.1","A"],["Gecko","Mozilla 1.2","Win 95+ / OSX.1+","1.2","A"],["Gecko","Mozilla 1.3","Win 95+ / OSX.1+","1.3","A"],["Gecko","Mozilla 1.4","Win 95+ / OSX.1+","1.4","A"],["Gecko","Mozilla 1.5","Win 95+ / OSX.1+","1.5","A"],["Gecko","Mozilla 1.6","Win 95+ / OSX.1+","1.6","A"],["Gecko","Mozilla 1.7","Win 98+ / OSX.1+","1.7","A"],["Gecko","Mozilla 1.8","Win 98+ / OSX.1+","1.8","A"],["Gecko","Seamonkey 1.1","Win 98+ / OSX.2+","1.8","A"],["Gecko","Epiphany 2.20","Gnome","1.8","A"]] }',
    '20' => '{"sEcho": __ECHO__, "iTotalRecords": 57, "iTotalDisplayRecords": 57, "aaData": [ ["KHTML","Konqureror 3.1","KDE 3.1","3.1","C"],["KHTML","Konqureror 3.3","KDE 3.3","3.3","A"],["KHTML","Konqureror 3.5","KDE 3.5","3.5","A"],["Misc","NetFront 3.1","Embedded devices","-","C"],["Misc","NetFront 3.4","Embedded devices","-","A"],["Misc","Dillo 0.8","Embedded devices","-","X"],["Misc","Links","Text only","-","X"],["Misc","Lynx","Text only","-","X"],["Misc","IE Mobile","Windows Mobile 6","-","C"],["Misc","PSP browser","PSP","-","C"]] }',
    '30' => '{"sEcho": __ECHO__, "iTotalRecords": 57, "iTotalDisplayRecords": 57, "aaData": [ ["Other browsers","All others","-","-","U"],["Presto","Opera 7.0","Win 95+ / OSX.1+","-","A"],["Presto","Opera 7.5","Win 95+ / OSX.2+","-","A"],["Presto","Opera 8.0","Win 95+ / OSX.2+","-","A"],["Presto","Opera 8.5","Win 95+ / OSX.2+","-","A"],["Presto","Opera 9.0","Win 95+ / OSX.3+","-","A"],["Presto","Opera 9.2","Win 88+ / OSX.3+","-","A"],["Presto","Opera 9.5","Win 88+ / OSX.3+","-","A"],["Presto","Opera for Wii","Wii","-","A"],["Presto","Nokia N800","N800","-","A"]] }',
    '40' => '{"sEcho": __ECHO__, "iTotalRecords": 57, "iTotalDisplayRecords": 57, "aaData": [ ["Presto","Nintendo DS browser","Nintendo DS","8.5","C/A1"],["Tasman","Internet Explorer 4.5","Mac OS 8-9","-","X"],["Tasman","Internet Explorer 5.1","Mac OS 7.6-9","1","C"],["Tasman","Internet Explorer 5.2","Mac OS 8-X","1","C"],["Trident","Internet Explorer 4.0","Win 95+","4","X"],["Trident","Internet Explorer 5.0","Win 95+","5","C"],["Trident","Internet Explorer 5.5","Win 95+","5.5","A"],["Trident","Internet Explorer 6","Win 98+","6","A"],["Trident","Internet Explorer 7","Win XP SP2+","7","A"],["Trident","AOL browser (AOL desktop)","Win XP","6","A"]] }',
    '50' => '{"sEcho": __ECHO__, "iTotalRecords": 57, "iTotalDisplayRecords": 57, "aaData": [ ["Webkit","Safari 1.2","OSX.3","125.5","A"],["Webkit","Safari 1.3","OSX.3","312.8","A"],["Webkit","Safari 2.0","OSX.4+","419.3","A"],["Webkit","Safari 3.0","OSX.4+","522.1","A"],["Webkit","OmniWeb 5.5","OSX.4+","420","A"],["Webkit","iPod Touch / iPhone","iPod","420.1","A"],["Webkit","S60","S60","413","A"]] }',
    );
    $ret = $response[$_GET['iDisplayStart']];
    $ret = str_replace('__ECHO__', $_GET['sEcho'], $ret);
    header('Content-Type: application/json');
    header('Content-Length: ' . strlen($ret));
    print $ret;
    ?>[/code]

    How did you initialize it on the table? This works just fine for me:
    [code] $(document).ready(function() {
    $('#example').dataTable( {
    "bProcessing": true,
    "bServerSide": true,
    "sAjaxSource": "http://localhost/datatables/datatables.php"
    } ).fnFakeRowspan(0);
    } );[/code]
  • allanallan Posts: 63,680Questions: 1Answers: 10,498 Site admin
    I think I've narrowed the issue down to a problem with my PHP script actually - sorry about that. The problem was I was using addslashes, which escapes a single quote, and that's not valid in strict JSON apparently. I've inserted a single quote to try and catch little issues like this :-). So in fact nothing wrong with your script at all - just me being daft! I'll post your function up shortly.

    Regards,
    Allan
  • vexvex Posts: 30Questions: 0Answers: 0
    Glad that you found the bug since I was beginning to wonder how it could just fail for you when it was working fine for me, specially when the code isn't complex.
  • allanallan Posts: 63,680Questions: 1Answers: 10,498 Site admin
    Hi vex,

    I've put the plug-in up on the plug-ins page: http://datatables.net/plug-ins/api#fnFakeRowspan . Thanks for taking the time to create and share the plug-in with us!

    Regards,
    Allan
  • pad31pad31 Posts: 1Questions: 0Answers: 0
    Hi every body,

    i have an issue with simple quote and addslashes function.. like your saying previously, the problem is that a single escaped quote isn't valid in strict JSON format. In order to solve the problem i used the addcslashes which allows you to choose which character you wanna escape. With this solution i didn't encounter the problem again.

    In all case, i would like to thank you for this wonderful plug-in but also for the other things you've done like all the web tools available on your website. I use them in my daily work and it's just awesome !

    Best regards.

    PS : Sorry for my english who has a little french accent.
  • kaarelkaarel Posts: 1Questions: 0Answers: 0
    edited September 2011
    I needed use rowspan on multiple columns (grouped by the value in one column), so I modified what vex had done to enable that. Also, I used .hide() instead of .remove() so I would be able to undo the rowspanning once the table is filtered or sorted. Here's what I came up with:

    [code]
    $.fn.dataTableExt.oApi.fnMultiRowspan = function ( oSettings, oSpannedColumns, bCaseSensitive ) {
    /*
    * Type: Plugin for DataTables (http://datatables.net) JQuery plugin.
    * Name: dataTableExt.oApi.fnMultiRowspan
    * Requires: DataTables 1.6.0+
    * Version: 0.7
    * Description: Creates rowspan cells in one or more columns when there are two or more cells in a row with the same
    * content.
    *
    * Inputs: object:oSettings - dataTables settings object
    * object:oSpannedColumns - the columns to fake rowspans in
    * boolean:bCaseSensitive - whether the comparison is case-sensitive or not (default: false)
    * Returns: JQuery
    * Usage: $('#example').dataTable().fnMultiRowspan([0]);
    * $('#example').dataTable().fnMultiRowspan({0: 0, 1: 0, 2: 0}, true);
    * $('#example').dataTable().fnMultiRowspan({"engine.name": "engine.name", "grade": "grade"});
    *
    * Author: Kaarel Nummert
    * Comment: Based on the fnFakeRowspan (http://datatables.net/plug-ins/api#fnFakeRowspan) plug-in created by Fredrik Wendel.
    * Created: 2011-09-02
    * Language: Javascript
    * License: GPL v2 or BSD 3 point style
    */
    var oSettings = oSettings,
    oSpannedColumns = oSpannedColumns,
    bCaseSensitive = (typeof(bCaseSensitive) != 'boolean' ? false : bCaseSensitive);

    oSettings.aoDrawCallback.push({ "fn": fnMultiRowspan, "sName": "fnMultiRowspan" });

    function fnMultiRowspan () {
    /* Reset rowspans. Should probably check if any of those columns are meant to be hidden. There is that option in DataTables, you know. */
    oSettings.oInstance.children("tbody").find("td").removeAttr("rowspan").show();

    /* Reset values on new cell data. */
    var firstOccurance = {},
    value = {},
    rowspan = {};

    for (i = 0; i < oSettings.aiDisplay.length; i++) {
    oData = oSettings.aoData[oSettings.aiDisplay[i]];
    for (key in oSpannedColumns) {
    var index = fnCellIndexByKey(key);

    if (oSpannedColumns[key] === null || index === null) {
    continue;
    }

    var cell = $($(oData.nTr).children().get(index)),
    comparisonKey = oSpannedColumns[key],
    val = oSettings.oApi._fnGetObjectDataFn(comparisonKey).call(oData, oData._aData);

    /* Use lowercase comparison if not case-sensitive. */
    if (!bCaseSensitive) {
    val = val.toLowerCase();
    }

    if (typeof value[key] == "undefined" || val != value[key]) {
    value[key] = val;
    firstOccurance[key] = cell;
    rowspan[key] = 1;
    } else {
    rowspan[key]++;
    }

    if (typeof firstOccurance[key] != "undefined" && val == value[key] && rowspan[key] > 1) {
    cell.hide();
    firstOccurance[key].attr("rowspan", rowspan[key]);
    }
    }
    }
    }

    function fnCellIndexByKey (key) {
    for (index in oSettings.aoColumns) {
    if (oSettings.aoColumns[index].mDataProp == key) {
    return index;
    }
    }
    return null;
    }

    /* Ensure rowspanning is done even if the table has already been drawn. */
    fnMultiRowspan();

    return this;
    }
    [/code]

    Only tested it with DataTables 1.8.1 and all modern browsers at my disposal - Firefox 6.0.1, Opera 11.51, Chrome 13.0.782.218, Safari 5.1, Internet Explorer 9.0.

    Can't see any reasons why it would have restrictions on data source, pagination, filtering or sorting. Although, when using it in conjunction with sorting, one has to keep in mind it does not do any data grouping of any sort. So if the same values appear in the comparison column in more than one block of rows, those blocks will be "rowspanned" separately. It probably will not work with column hiding, and I assume a native JS speaker can make it quite a bit more elegant.

    EDITED: September 5th, 2011

    I made some significant improvements to it:

    1. Instead of specifying one particular comparison column, user can now specify a separate comparison column to every to-be-spanned column.
    2. Instead of only supporting column indices, one can now use the (nested) keys of data input. Especially useful if data is given as object.
    3. Removed option to hide comparison column (one may not consider this as an improvement, but the two improvements above made it obsolete).

    Example to show off the improvement No 1 and No 2:
    [code]
    $(document).ready(function() {
    var oTable = $('#example').dataTable({
    aoColumns: [
    {mDataProp: "engine.name"},
    {mDataProp: "browser"},
    {mDataProp: "platform"},
    {mDataProp: "engine.version"},
    {mDataProp: "grade"},
    ]
    }).makeEditable().fnMultiRowspan({"engine.name": "engine.name", "engine.version": "engine.name", "grade": "grade"});
    oTable.fnAddData([
    {engine: {name: "Gecko", version: "1.7"}, browser: "Firefox 1.0", platform: "Win 98+ / OSX.2+", grade: "A"},
    {engine: {name: "Gecko", version: "1.8"}, browser: "Firefox 1.5", platform: "Win 98+ / OSX.2+", grade: "A"},
    {engine: {name: "Gecko", version: "1.8"}, browser: "Firefox 2.0", platform: "Win 98+ / OSX.2+", grade: "A"},
    {engine: {name: "Gecko", version: "1.9 "}, browser: "Firefox 3.0", platform: "Win 98+ / OSX.2+", grade: "A"}
    ]);
    } );
    [/code]
  • joenathan523joenathan523 Posts: 1Questions: 0Answers: 0
    As implied in the comments, fnMultiRowspan is not quite tolerant of hidden columns; when DataTables columns are hidden, they are actually removed from the table, so numeric column indices are no longer accurate. To combat this, I added to the fnMultiRowspan callback method an iteration through all hidden columns, and for each one, I decrement all subsequent numeric oSpannedColumns indices and values. I'm not sure if the problem exists for string column indices (such as "engine.name", etc. above), but at worst, functionality for string column indices should be unchanged.

    This is my first time contributing to anything like open source code, so constructive criticism is welcome.

    [code]
    $.fn.dataTableExt.oApi.fnMultiRowspan = function ( oSettings, oSpannedColumns, bCaseSensitive ) {
    /*
    * Type: Plugin for DataTables (http://datatables.net) JQuery plugin.
    * Name: dataTableExt.oApi.fnMultiRowspan
    * Requires: DataTables 1.6.0+
    * Version: 0.7
    * Description: Creates rowspan cells in one or more columns when there are two or more cells in a row with the same
    * content.
    *
    * Inputs: object:oSettings - dataTables settings object
    * object:oSpannedColumns - the columns to fake rowspans in
    * boolean:bCaseSensitive - whether the comparison is case-sensitive or not (default: false)
    * Returns: JQuery
    * Usage: $('#example').dataTable().fnMultiRowspan([0]);
    * $('#example').dataTable().fnMultiRowspan({0: 0, 1: 0, 2: 0}, true);
    * $('#example').dataTable().fnMultiRowspan({"engine.name": "engine.name", "grade": "grade"});
    *
    * Author: Kaarel Nummert
    * Comment: Based on the fnFakeRowspan (http://datatables.net/plug-ins/api#fnFakeRowspan) plug-in created by Fredrik Wendel.
    * Created: 2011-09-02
    * Language: Javascript
    * License: GPL v2 or BSD 3 point style
    */
    var oSettings = oSettings,
    oSpannedColumns = oSpannedColumns,
    bCaseSensitive = (typeof(bCaseSensitive) != 'boolean' ? false : bCaseSensitive);

    oSettings.aoDrawCallback.push({ "fn": fnMultiRowspan, "sName": "fnMultiRowspan" });

    function fnMultiRowspan () {
    /* Reset rowspans. Should probably check if any of those columns are meant to be hidden. There is that option in DataTables, you know. */
    oSettings.oInstance.children("tbody").find("td").removeAttr("rowspan").show();

    // Need to move all numeric keys and values one to the left where > hidden col index
    var oSpannedColumnsTemp = new Object();
    for (key in oSpannedColumns)
    oSpannedColumnsTemp[key] = oSpannedColumns[key];
    for (index in oSettings.aoColumns) {
    var indexInt = parseInt(index);
    if (typeof(indexInt) == 'number' && !oSettings.aoColumns[index].bVisible) {
    var oSpannedColumnsKeyTransitions = new Object(); // Map of previous key to new key
    for (key in oSpannedColumnsTemp) {
    var newVal = oSpannedColumnsTemp[key];
    var newValInt = parseInt(newVal);
    if (typeof(newValInt) == 'number' && newValInt > indexInt)
    oSpannedColumnsTemp[key] = '' + (newValInt-1);
    var keyInt = parseInt(key);
    if (typeof(keyInt) == 'number' && keyInt > indexInt)
    oSpannedColumnsKeyTransitions[key] = '' + (keyInt-1);
    }
    for (key in oSpannedColumnsKeyTransitions) {
    oSpannedColumnsTemp[oSpannedColumnsKeyTransitions[key]] = oSpannedColumnsTemp[key];
    delete oSpannedColumnsTemp[key];
    }
    }
    }

    /* Reset values on new cell data. */
    var firstOccurance = {},
    value = {},
    rowspan = {};

    for (i = 0; i < oSettings.aiDisplay.length; i++) {
    oData = oSettings.aoData[oSettings.aiDisplay[i]];
    for (key in oSpannedColumnsTemp) {
    var index = fnCellIndexByKey(key);

    if (oSpannedColumnsTemp[key] === null || index === null)
    continue;

    var cell = $($(oData.nTr).children().get(index)),
    comparisonKey = oSpannedColumnsTemp[key],
    val = oSettings.oApi._fnGetObjectDataFn(comparisonKey).call(oData, oData._aData);

    /* Use lowercase comparison if not case-sensitive. */
    if (!bCaseSensitive)
    val = val.toLowerCase();

    if (typeof value[key] == "undefined" || val != value[key]) {
    value[key] = val;
    firstOccurance[key] = cell;
    rowspan[key] = 1;
    } else
    rowspan[key]++;

    if (typeof firstOccurance[key] != "undefined" && val == value[key] && rowspan[key] > 1) {
    cell.hide();
    firstOccurance[key].attr("rowspan", rowspan[key]);
    }
    }
    }
    }

    function fnCellIndexByKey (key) {
    for (index in oSettings.aoColumns) {
    if (oSettings.aoColumns[index].mDataProp == key) {
    return index;
    }
    }
    return null;
    }

    /* Ensure rowspanning is done even if the table has already been drawn. */
    fnMultiRowspan();

    return this;
    }
    [/code]
This discussion has been closed.