Makes tables sortable macro: only available to users with programming rights?

Hello @Ricardo_Rodriguez,

I don’t see any open issue with this bug on Loading...
You can create a bug issue to increase your chances to see it fixed someday.

Note that this extension is not recommended and is maintained by @ClemensRobbenhaar.

We do use that extension and with 15.10.3 I can not confirm that issue.
However in our Installation I modified the extension slightly it, so that it don’t enforce the table.css because I like the current modern table design of XWiki much more. The extension probably was created when XWiki’s tables looked much different. I don’t know if that modification is preventing the issue?

The change I did was:

  1. open /xwiki/bin/edit/Macros/SortableTables?editor=object
  2. Modify the XWiki.WikiMacroClass object
  3. comment out the line “$xwiki.ssfx.use(“js/xwiki/table/table.css”)” with writing “##” before it.

I really appreciate the work of the extension. I would love to see an updated or similar implementation directy included into XWiki. That would making the transfer from Confluence much easier and be a big QOL improvement. The filters should however be inside the header row and not before it, and it should generally work flawlessly with every XWiki theme - I can only guess that this cannot be easily be done with an extension and would mean much deeper modifications in XWiki.

1 Like

Thanks. I’m just trying it in my way to feed Live Data Macro with external SQL sources. I will create a Jira issue.

ok, I did a quick try on my end with the following steps, I could make it work for a standard user

  1. login as a standard user (not programming or script rights)
  2. create a page with the following content
|A|B
|1|10
|10|1

{{sortable_tables/}}
  1. try to sort the table on column A and B
  2. clear the browser cache (Ctrl+Shift+R)

Thanks, @TomTheWise! I applied the same changes here and I’m now enjoying the current modern table design of XWiki! It was one of the things I didn’t like about Tables Sortable Macro. But the filters are still only available for logged in users with admin rights. Regular users didn’t see it. I will play a bit before creating the Jira issue.

1 Like

It is also working for me in a XWiki 15.9 instance on AlmaLinux 9.3 that I have hardly touched upon installation:

image

And failing in the XWiki 15.10.4 one on Ubuntu 22.04.03 LTS I’m mostly playing one.

image

Thus, the issue seems on my side, although I’m not the first one on seeing it. Perhaps some particular dependency of the Sortable Table Macro? Could you help me to debug this issue? It is worth a Jira issue?

I see that 15.10.5 release is available. I will wait for a while, looking for feedback here, before attempting to update.

Thanks!

Hi! It is not clear to me what must I report on that issue. The problem seems related with rights, but I’m not able to understand what to ask. Two facts:

  1. If I grant Programming rights to Guest, sorting and filtering work. This way:
    image

  2. These setting also allow Register, although these rights are explicitly DENIED to Guest
    image

  3. Also, these settings show several Applications in the right column, although most of them will require login at any moment.
    image

I understand that XWiki rights system is are very complex, but I’m not able to understand what changes I did that prevent Sortable Table Macro to work properly.

Is it possible to know what are the required permissions for Sortable Table Macro to work properly? @ClemensRobbenhaar, please, could you help me with this? Thanks!

Uh, oh, I missed the discussion somehow.

The “Sortable Tables” extension must be installed with an admin having Programming rights.
There should be absolutely no need for the users of that macro to have programming rights (especially not for the “Guest” user - that is a very bad idea, as any anonymous person can create scripts that crete and Admin account or do all kind of stuff on the server that XWiki is running on).

So the expected state is:

  • to install the extension the admin need to have “Programming” rights.
  • users with edit rights for a page can use the “sortable table” macro to insert it in a page
  • the macro should work if a user with “View” rights look at any page they have view right where the macro is used.
1 Like

@ClemensRobbenhaar, thanks for your answer! It is by no means easy to follow everything happening in XWiki ecosystem with proper refences from users!

The installer was the owner and, at that time, the only user of the XWiki instance: me! Another use is helping me now test/debug issues at this very early implementation stage.

Agree! I’ve already removed that settings. I was just playing with the installation and saw that it works and

What I did some minutes ago was:

  1. Explicitly granted Programming rights to my user.
  2. Uninstalled “Make Tables Sortable Macro”.
  3. And… surprise! The macro seems to be still available for me! See below some screenshots.

No macro shows up when I look for the string “Sort”:

image

By using the “Existing wiki macro definitions”, it shows up!

image

And, as you see in the image above, filters and sorting options are available for the table. Thus, “Make Tables Sortable Macro” is available! I added the macro to that page.

I’m lost! I’m not able to understand what I did to generate the, at least for me, current chaos! Any idea will be more than welcome! Thanks!

Try to Manually delete the macro related pages and then try to install freshly with the extension manager.

My guess would be that the macro pages themself have so wrong permissions set.

Thanks, @TomTheWise. I did it. Same results :frowning:

This is not the only weird issue I’m facing in this XWiki instance. Thus, it is clear that I changed something that leads to the wrong state. I’m just trying to understand what I did to prevent others and also be able to revert any wrong action.

History at Global Administration shows all installation and deletions of extensions. Perhaps I could try to be back to the original installation. I can also list all objects of type XWiki.XWikiRights I added, order them by date, and delete one by one trying to find when I added the one, if any, preventing “Makes tables sortable macro” to work for not admin users.

Any other idea that could help to debug this issue? Thanks!

To list all rights of pages I made this script some times ago: How to see all the permissions sets on the Xwiki?.

Maybe this is a good starting point for you.
Simpel

2 Likes

Thanks, @Simpel! I think it was. I have no idea about why it works this way, but I think I find a pattern.

If I add the Makes tables sortable macro to a page containing or generating a table, if the table has an id, like the generated by your snippet, it appears as sortable and filterable if set that way to any user, included XWiki.XWikiGuest. If the table has no id, like mine generated with a Groovy script, it will only appear as sortable and filterable to Admin users, or to the users explicitly granted View rights to the macro’s page.

These two examples are currently available online. I’m adding screenshot as evidences as probably I will remove or change them shortly:

Filtering and sorting is working here for Guest user:

https://portal.igfae.usc.es/xwiki/bin/view/List%20all%20rigths/

image

But it is not working in this other page:

https://portal.igfae.usc.es/xwiki/bin/view/Team/

image

To test this further, I need to learn how to add an id to tables generated with Groovy. I will do my best and report back with the results! In the meantime, any idea or help will be very welcome! Thanks!

Hihi. My snippet was for solving your idea below:

What you found out wasn’t intended but it’s cool if it could nudge you in a good direction.

Maybe an id can made this way in velocity?

println "{{id name='test'/}}"

Right ontop of your tables header. Or use a random number as id:

randomnumber = Math.abs(new Random().nextInt() % 99999)

Thanks, @Simpel! I’m afraid it won’t be that easy, :slight_smile: I need to catch up with the HTML writing tools with Velocity and Groovy.

Besides, I found another thing I can not explain so far: both the Makes tables sortable macro and the table heading as it reads in your script to read all rights objects (see below) are required for getting a filterable and sortable table.

<table id="tableid${velocityCount}" class="grid sortable filterable doOddEven">

It seems that there are some parts of the macro that work when explicit view rights are granted for guest and are also useful to make the table generated by your script filterable and sortable. As far as read, your script should generate a filterable/sortable on its own. It doesn’t happen here: it needs the Makes tables sortable macro.

Does this make any sense to you? Thanks!

We don’t use a macro for the sortable table etc. We have a page with java script extension and style sheet extension.

The java script extension code is:

//$xwiki.jsfx.use("js/xwiki/table/tablefilterNsort.js", true)
//var imported = document.createElement('script');
//imported.src = '/resources/js/xwiki/table/tablefilterNsort.js';
//document.head.appendChild(imported);

/*
    Note: We got the agreement from Max Guglielmi (on 21/5/2007) to include this script in XWiki's
          distribution. Same from Jan Eldenmalm. The script from Joost de Valk is under the MIT
          license.
    Source: http://www.eldenmalm.com/tableFilterNSort.jsp
    Source: http://mguglielmi.free.fr/scripts/TableFilter/
*/
/*#####################################################
    Version: 1.5
    Date: 6 Aug 2006
    Editor & code Fixes: Jan (at) Eldenmalm . com
    Coders: Joost de Valk, Max Guglielmi
    This script is a merger of two brilliant scripts
    and some improvements to make them easier to use.
    With this one script it is easy to filter and sort
    a table at client side without adding anything but
    "classes" to the TD and TR tags and a link this script.
    This joint script still contains legacy code from the two
    original coders  - for functionality that currently is untested.

    Documentation of currently tested functionality:

    Example:
    <table class="sortable filterable doOddEven" id="myUniqueTableId">
    <tr><td></td><td></td><td></td></tr>
    <tr><td></td><td></td><td></td></tr>
    <tr class="sortHeader"><td class="selectFilter"></td><td></td><td class="unsortable noFilter"></td></tr>
    <tr><td></td><td></td><td></td></tr>
    <tr><td></td><td></td><td></td></tr>
    <tr class="sortBottom"><td></td><td></td><td></td></tr>
    <tr><td></td><td></td><td></td></tr>
    <tr><td></td><td></td><td></td></tr>
    </table>

    CLASS names and their meaning:
    id (table)(mandatory) = a unique identifier not shared with other objects.
    sortable (table)(optional) = Makes the table sortable
    filterable (table)(optional) = Makes the table filterable
    doOddEven (table) (optional) = declares that visible table rows between the sortHeader row and
                                   sortBottom should contain class names "odd" and "even"
                                   for creating alternating tables
    sortHeader (tr)(mandatory) = is the row in the table that is the header row
    noFilter (td)(optional) =  declares a column as non filterable
    selectFilter (td) (optional)  = declares a column as filterable using a selct box,
                                    that is autimatically populated with "available" line values
    unsortable (td) (optional) = declares a column as non sortable
    sortBottom (tr) (optional) = declares a row as the bottom of the sortable rows ( used for totals etc...)

    Notice: Images are sorted on the "alt" attribute.

    The sort function required access to three a few pictures to "look good"
    Change these values with regards to your specific set up */
    var image_path = "";
    var image_up = "/resources/js/xwiki/table/img/arrow-up.gif";
    var image_down = "/resources/js/xwiki/table/img/arrow-down.gif";
    var image_none = "/resources/js/xwiki/table/img/arrow-none.gif";

/*====================================================
  - HTML Table Filter Generator v1.3.jan1
  - By Max Guglielmi
  - mguglielmi.free.fr/scripts/TableFilter/?l=en
  - please do not change this comment
  - don't forget to give some credit... it's always
  good for the author
=====================================================*/

/*
Table sorting script, taken from http://www.kryogenix.org/code/browser/sorttable/ .
Distributed under the MIT license: http://www.kryogenix.org/code/browser/licence.html .

Adaptation by Joost de Valk ( http://www.joostdevalk.nl/ ) to add alternating row classes as well.

Copyright (c) 1997-2006 Stuart Langridge, Joost de Valk.
*/


/* Don't change anything below this unless you know what you're doing */

var TblId, StartRow, SearchFlt, ModFn, ModFnId;
TblId = new Array(), StartRow = new Array();
ModFn = new Array(), ModFnId = new Array();

// creates and event that triggers onload and enables the filtering and sorting.
addEvent(window, "load", init_sortnfilter);

var SORT_COLUMN_INDEX;

function init_sortnfilter() {
// Intiates the sorting and filtering triggered on an event
  sortables_init();
  filterable_init();
}

function sortables_init() {
// Initiates the sorting capability
  // Find all tables with class sortable and make them sortable
  if (!document.getElementsByTagName) {
    return;
  }
  var tbls = document.getElementsByTagName("table");
  for (var ti = 0; ti < tbls.length; ti++) {
    var thisTbl = tbls[ti];
    if (((' ' + thisTbl.className + ' ').indexOf("sortable") != -1) && (thisTbl.id)) {
      //initTable(thisTbl.id);
      ts_makeSortable(thisTbl);
      //sum_up(thisTbl);
    }
  }
}

function filterable_init() {
// Initiates the filtering capability
  // Find all tables with class doFilter and make them filterable
  if (!document.getElementsByTagName) {
    return;
  }
  var tbls = document.getElementsByTagName("table");
  for (var ti = 0; ti < tbls.length; ++ti) {
    var thisTbl = tbls[ti];
    if (((' '+thisTbl.className+' ').indexOf("filterable") != -1) && (thisTbl.id)) {
      //initTable(thisTbl.id);
      //ts_makeSortable(thisTbl);
      setFilterGrid(thisTbl.id, (getHeaderRow(thisTbl)));
      //sum_up(thisTbl);
    }
  }
}

function ts_resortTable(lnk) {
  // get the span
  var span, ARROW;
  for (var ci = 0; ci < lnk.childNodes.length; ++ci) {
    if (lnk.childNodes[ci].tagName && lnk.childNodes[ci].tagName.toLowerCase() == 'span') span = lnk.childNodes[ci];
  }
  var spantext = ts_getInnerText(span);
  var td = lnk.parentNode;
  var column = td.cellIndex;
  var table = getParent(td,'TABLE');

  // Work out a type for the column
  if (table.rows.length <= 1) {
    return;
  }
  var itm = ts_getInnerText(table.rows[getHeaderRow(table)+1].cells[column]);
  var nextRow = getHeaderRow(table) + 2;
  // this loop will get the contents from the first line with actual content
  while (!itm) {
    itm = ts_getInnerText(table.rows[nextRow].cells[column]) ;
    ++nextRow;
  }
  // Ensures for sorting and evaluation purposes that itm is never undefined
  if (!itm) {
    itm = '';
  }
  var sortfn = ts_sort_caseinsensitive;
  if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) {
    sortfn = ts_sort_date;
  }
  if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) {
    sortfn = ts_sort_date;
  }
  if (itm.match(/^[£$]/)) {
    sortfn = ts_sort_currency;
  }
  if (itm.match(/^[\d\.]+$/)) {
    sortfn = ts_sort_numeric;
  }
  SORT_COLUMN_INDEX = column;
  var firstRow = new Array();
  var newRows = new Array();
  var nonSortedRows = new Array();
  // the new rows are added to the array to sort...get all rows from the "sortHeader" to the first sortBottom row.
  var firstNonSortedRow = null != getSortBottomRow(table) ? (getSortBottomRow(table)+1) : table.rows.length ;

  for (var j = firstNonSortedRow; j < table.rows.length; ++j) {
    nonSortedRows[nonSortedRows.length] = table.rows[j];
  }

  for (var j = getHeaderRow(table) + 1; j < table.rows.length; ++j) {
    newRows[newRows.length] = table.rows[j];
  }
  newRows.sort(sortfn);

  if (span.getAttribute("sortdir") == 'down') {
    ARROW = '<img border="0" src="'+ image_path + image_up + '" alt=""/>';
    newRows.reverse();
    span.setAttribute('sortdir','up');
  } else {
    ARROW = '<img border="0" src="'+ image_path + image_down + '" alt=""/>';
    span.setAttribute('sortdir','down');
  }

  // We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
  // don't do sortbottom rows
  for (var i = 0; i < newRows.length; ++i) {
    if (!newRows[i].className || (newRows[i].className && (newRows[i].className.indexOf('sortBottom') == -1))) {
      table.tBodies[0].appendChild(newRows[i]);
    }
  }
  // do sortBottom rows only
  for (var i=0; i<newRows.length; i++) {
    if (newRows[i].className && (newRows[i].className.indexOf('sortBottom') != -1)) {
      table.tBodies[0].appendChild(newRows[i]);
    }
  }
  // ad the non sorted rows...
  for (var i = 0; i < nonSortedRows.length; ++i) {
    table.tBodies[0].appendChild(nonSortedRows[i]);
  }

  // Delete any other arrows there may be showing
  var allspans = document.getElementsByTagName("span");
  for (var ci = 0; ci < allspans.length; ++ci) {
    if (allspans[ci].className == 'sortarrow') {
      if (getParent(allspans[ci],"table") == getParent(lnk,"table")) { // in the same table as us?
        allspans[ci].innerHTML = '<img border="0" src="'+ image_path + image_none + '" alt=""/>';
      }
    }
  }

  span.innerHTML = ARROW;
  alternate(table);
}

function Filter(id) {
/*====================================================
  - gets search strings from SearchFlt array
  - retrieves data from each td in every single tr
  and compares to search string for current
  column
  - tr is hidden if all search strings are not
  found
=====================================================*/
  getFilters(id);
  var t = document.getElementById(id);
  var SearchArgs = new Array();
  var ncells = getCellsNb(id);
  var visibleRows = 1 ;

  // Vincent Massol: Modified orinigal line so that it's compatible with Prototype.
  // Note: this is fixed in Max's script at http://mguglielmi.free.fr/scripts/TableFilter/?l=fr
  // Original: for(var i in SearchFlt) SearchArgs.push((document.getElementById(SearchFlt[i]).value).toLowerCase());
  for (var i = 0; i < SearchFlt.length; ++i) {
    SearchArgs.push((document.getElementById(SearchFlt[i]).value).toLowerCase());
  }

  var start_row = getStartRow(id);
  var row = t.getElementsByTagName("tr");
  var lastFilteredRow = null != getSortBottomRow(t) ? getSortBottomRow(t) : t.rows.length;

  for(var k = start_row; k < row.length; ++k) {
    var isRowValid = true;
    /*** if table already filtered some rows are not visible ***/
    if(row[k].style.display == "none") {
      row[k].style.display = "";
    }

    var cell = getChildElms(row[k]).childNodes;
    var nchilds = cell.length;
    if (nchilds == ncells) {// checks if row has exact cell #
      var cell_value = new Array();
      var occurence = new Array();

      for (var j=0; j<nchilds; j++) {// this loop retrieves cell data
        var cell_data = getCellText(cell[j]).toLowerCase();
        cell_value.push(cell_data);

        if (SearchArgs[j] != "") {
          var num_cell_data = parseFloat(cell_data);
          if(/<=/.test(SearchArgs[j]) && !isNaN(num_cell_data)) {// first checks if there is an operator (<,>,<=,>=)
            num_cell_data <= parseFloat(SearchArgs[j].replace(/<=/, "")) ? occurence[j] = 3 : occurence[j] = 1;
          } else if(/>=/.test(SearchArgs[j]) && !isNaN(num_cell_data)) {
            num_cell_data >= parseFloat(SearchArgs[j].replace(/>=/,"")) ? occurence[j] = 3 : occurence[j] = 1;
          } else if(/</.test(SearchArgs[j]) && !isNaN(num_cell_data)) {
            num_cell_data < parseFloat(SearchArgs[j].replace(/</,"")) ? occurence[j] = 3 : occurence[j] = 1;
          } else if(/>/.test(SearchArgs[j]) && !isNaN(num_cell_data)) {
            num_cell_data > parseFloat(SearchArgs[j].replace(/>/,"")) ? occurence[j] = 3 : occurence[j] = 1;
          } else {
            occurence[j] = cell_data.split(SearchArgs[j]).length;
          }
        }
      }//for j

      for(var u=0; u<ncells; u++) {
        if(SearchArgs[u]!="" && occurence[u]<2) {
          isRowValid = false;
        }
      }//for t
    }//if

    if(isRowValid==false && k < lastFilteredRow) {
      row[k].style.display = "none";
    } else {
      row[k].style.display = "";
    }
  }// for k
  // After filtering call the alternation function to alternate...
  alternate(t);
}

//  Subroutines below

function getHeaderRow (table) {
// This function takes a table and verifies in which row the class name of <TR> tag contains "sortHeader"
// When found it returns this row....i.e the header row - the next row is normally the first sortable row
// if no row is found or the sort header is the "last" row - than it returns 0.
  for (var i = 0; i < table.rows.length - 1; ++i) {
    if (table.rows[i].className.indexOf("sortHeader") > -1) {
      return i ;
    }
  }
  return 0 ;
}

function getSortBottomRow (table) {
// This function takes a table and verifies in which row the class name of <TR> tag contains "sortHeader"
// When found it returns this row....i.e the header row - the next row is normally the first sortable row
// if no row is found or the sort header is the "last" row - than it returns 0.
  for (var i = 0; i < table.rows.length; ++i) {
    if (table.rows[i].className.indexOf("sortBottom") > -1) {
      return i;
    }
  }
  return null ;
}

function ts_getInnerText(el) {
  if (typeof el == "string") {
    return el;
  }
  if (typeof el == "undefined") {
    return ''
  };
  if (typeof el == "object" && el.tagName.toLowerCase() == 'img') {
    // if the contents of the table are images - they can be sorted on the "Alt" text
    return el.alt ;
  }
  if (el.innerText) {
    return el.innerText;  //Not needed but it is faster
  }
  var str = "";

  var cs = el.childNodes;
  var l = cs.length;
  for (var i = 0; i < l; i++) {
    switch (cs[i].nodeType) {
      case 1: //ELEMENT_NODE
        str += ts_getInnerText(cs[i]);
        break;
      case 3: //TEXT_NODE
        str += cs[i].nodeValue;
        break;
    }
  }
  return str;
}

function ts_makeSortable(table) {
  var firstRow;
  if (table.rows && table.rows.length > 0) {
    firstRow = table.rows[getHeaderRow(table)];
  }
  if (!firstRow) {
    return;
  }

  // We have a first row: assume it's the header, and make its contents clickable links
  for (var i = 0; i < firstRow.cells.length; ++i) {
    var cell = firstRow.cells[i];
    var txt = ts_getInnerText(cell);
    if (cell.className != "unsortable" && cell.className.indexOf("unsortable") == -1) {
      cell.innerHTML = '<a href="#" class="sortHeader" onclick="ts_resortTable(this);return false;">' + txt + '<span class="sortarrow"><img border="0" src="'+ image_path + image_none + '" alt=""/></span></a>';
    }
  }
  alternate(table);
}

function alternate(table) {
/*====================================================
  - check if the table passed to the function is set
  to doOddEven, if that is the case it colors the lines acordingly.
=====================================================*/
  if (table.className.indexOf("doOddEven") > -1) {
    var visibleRows = 1;
    var endAlternation = null != getSortBottomRow(table) ? getSortBottomRow(table) : table.rows.length ;
    // Take object table and get all it's tbodies.
    var tableBodies = table.getElementsByTagName("tbody");
    // Loop through these tbodies
    for (var i = 0; i < tableBodies.length; i++) {
      // Take the tbody, and get all it's rows
      var tableRows = tableBodies[i].getElementsByTagName("tr");
      // Loop through these rows
      // Start at 1 because we want to leave the heading row untouched
      // we change this to start at 2 to leave two rows untouched
      for (var j = getHeaderRow(table) + 1; j < endAlternation; ++j) {
        // Check if j is even, and apply classes for both possible results
        if (tableRows[j].style.display=="") {
          visibleRows++ ;
          swapOddEven(tableRows[j],visibleRows)
        }
      }
    }
  }
}

function getParent(el, pTagName) {
  if (el == null) {
    return null;
  } else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase()) {  // Gecko bug, supposed to be uppercase
    return el;
  } else {
    return getParent(el.parentNode, pTagName);
  }
}

function addEvent(elm, evType, fn, useCapture) {
// addEvent and removeEvent
// cross-browser event handling for IE5+,  NS6 and Mozilla
// By Scott Andrew
  if (elm.addEventListener) {
    elm.addEventListener(evType, fn, useCapture);
    return true;
  } else if (elm.attachEvent) {
    var r = elm.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be removed");
  }
}

function replace(s, t, u) {
  /*
  **  Replace a token in a string
  **    s  string to be processed
  **    t  token to be found and removed
  **    u  token to be inserted
  **  returns new String
  */
  var i = s.indexOf(t);
  var r = "";
  if (i == -1) {
    return s;
  }
  r += s.substring(0,i) + u;
  if ( i + t.length < s.length) {
    r += replace(s.substring(i + t.length, s.length), t, u);
  }
  return r;
}

function setFilterGrid(id) {
/*====================================================
  - Checks if id exists and is a table
  - Then looks for additional params
  - Calls fn that adds inputs and button
=====================================================*/
  var tbl = document.getElementById(id);
  var ref_row, fObj;
  if(tbl != null && tbl.nodeName.toLowerCase() == "table") {
    TblId.push(id);
    if (arguments.length>1) {
      for(var i=0; i<arguments.length; i++) {
        var argtype = typeof arguments[i];

        switch(argtype.toLowerCase()) {
          case "number":
            ref_row = arguments[i];
          break;
          case "object":
            fObj = arguments[i];
          break;
        }//switch
      }//for
    }//if

    ref_row == undefined ? StartRow.push(2) : StartRow.push(ref_row+2);
    var ncells = getCellsNb(id,ref_row);
    AddRow(id,ncells,fObj);
  }
}

function AddRow(id,n,f) {
/*====================================================
  - adds a row containing the filtering grid
=====================================================*/
  var t = document.getElementById(id);
  var fltrow = t.insertRow(0);
  // get the filtering settings from the header row...
  var checkRow = t.rows[getHeaderRow(t)];
  //var checkRow = t.rows[1];
  var inpclass, displayBtn, btntext, enterkey, modfilter_fn;

  f!=undefined && f["btn"]==false ? displayBtn = false : displayBtn = true;
  f!=undefined && f["btn_text"]!=undefined ? btntext = f["btn_text"] : btntext = "Filter";
  f!=undefined && f["enter_key"]==false ? enterkey = false : enterkey = true;
  f!=undefined && f["mod_filter_fn"] ? modfilter_fn = true : modfilter_fn = false;
  if(modfilter_fn) {
    ModFnId.push(id);
    ModFn.push(f["mod_filter_fn"]);
  }

  for(var i = 0; i < n; ++i) {
    var fltcell = fltrow.insertCell(i);
    var checkCell = checkRow.cells[i] ;
    i==n-1 && displayBtn==true ? inpclass = "flt_s" : inpclass = "flt";
    // this IF statement is mixture between the "old" way of initialising the filtering and the new way based on classes - not nice. :(
    if ((f == undefined || f["col_"+i] == undefined || f["col_"+i] == "none") && checkCell.className.indexOf("selectFilter") == -1) {
      var inp = document.createElement("input");
      inp.setAttribute("id", "flt" + i + "_" + id);
      inp.setAttribute("size", "5");
      if (checkCell.className.indexOf("noFilter") == -1) {
        inp.setAttribute("type","text");
      } else {
        inp.setAttribute("type","hidden");
      }
      // inp.setAttribute("class","flt"); //doesn't seem to work on ie<=6
      inp.className = inpclass;
      fltcell.appendChild(inp);
      if (enterkey) {
        inp.onkeypress = DetectKey;
      }
    } else if(checkCell.className.indexOf("selectFilter") != -1) {
      // create a select box and popultate it of the column is marked to do a "selectFilter"
      var slc = document.createElement("select");
      slc.setAttribute("id", "flt" + i + "_" + id);
      slc.className = inpclass;
      fltcell.appendChild(slc);
      PopulateOptions(id, i, n);
      if (enterkey) {
        slc.onkeypress = DetectKey;
      }
    }

    if (i == n-1 && displayBtn == true) {// this adds button
      var btn = document.createElement("input");

      btn.setAttribute("id", "btn" + i + "_" + id);
      btn.setAttribute("type", "button");
      btn.setAttribute("value", btntext);
      btn.className = "btnflt";

      fltcell.appendChild(btn);
      (!modfilter_fn) ? btn.onclick = function() { Filter(id) } : btn.onclick = f["mod_filter_fn"];
    }//if
  }// for i
}

function PopulateOptions(id, cellIndex, ncells) {
/*====================================================
  - populates select
  - adds only 1 occurence of a value
=====================================================*/
  var t = document.getElementById(id);
  var start_row = getStartRow(id);
  var row = t.getElementsByTagName("tr");
  var OptArray = new Array();
  var optIndex = 0; // option index

  for (var k = start_row; k < row.length; ++k) {
    var cell = getChildElms(row[k]).childNodes;
    var nchilds = cell.length;

    if (nchilds == ncells) {// checks if row has exact cell #
      for (var j = 0; j < nchilds; ++j) {// this loop retrieves cell data
        if (cellIndex == j) {
          var cell_data = getCellText(cell[j]);
          if (OptArray.toString().toUpperCase().search(cell_data.toUpperCase()) == -1) {
            // checks if celldata is already in array
            optIndex++;
            OptArray.push(cell_data);
            var currOpt = new Option(cell_data,cell_data,false,false);
            document.getElementById("flt"+cellIndex+"_"+id).options[optIndex] = currOpt;
          }
        }//if cellIndex==j
      }//for j
    }//if
  }//for k
}

function getCellsNb(id, nrow) {
/*====================================================
  - returns number of cells in a row
  - if nrow param is passed returns number of cells
  of that specific row
=====================================================*/
  var t = document.getElementById(id);
  var tr;
  if (nrow == undefined) {
    tr = t.getElementsByTagName("tr")[0];
  } else {
    tr = t.getElementsByTagName("tr")[nrow];
  }
  var n = getChildElms(tr);
  return n.childNodes.length;
}

function getFilters(id) {
/*====================================================
  - filter (input or select) ids are stored in
  SearchFlt array
=====================================================*/
  SearchFlt = new Array();
  var t = document.getElementById(id);
  var tr = t.getElementsByTagName("tr")[0];
  var enfants = tr.childNodes;

  for (var i = 0; i < enfants.length; ++i) {
    SearchFlt.push(enfants[i].firstChild.getAttribute("id"));
  }
}

function getStartRow(id) {
/*====================================================
  - returns starting row for Filter fn for a
  given table id
=====================================================*/
  var r;
  for(var j in TblId) {
    if(TblId[j] == id) {
      r = StartRow[j];
    }
  }
  return r;
}

function getChildElms(n) {
/*====================================================
  - checks passed node is a ELEMENT_NODE nodeType=1
  - removes TEXT_NODE nodeType=3
=====================================================*/
  if (n.nodeType == 1) {
    var enfants = n.childNodes;
    for(var i = 0; i < enfants.length; ++i) {
      var child = enfants[i];
      if (child.nodeType == 3) {
        n.removeChild(child);
      }
    }
    return n;
  }
}

function getCellText(n) {
/*====================================================
  - returns text + text of child nodes of a cell
=====================================================*/
  var s = "";
  var enfants = n.childNodes;
  for(var i = 0; i < enfants.length; ++i) {
    var child = enfants[i];
    if (child.nodeType == 3) {
      s += child.data;
    } else {
      s += getCellText(child);
    }
  }
  return s;
}

function DetectKey(e) {
/*====================================================
  - common fn that detects return key for a given
  element (onkeypress attribute on input)
=====================================================*/
  var evt = (e) ? e : (window.event) ? window.event : null;
  if (evt) {
    var key = (evt.charCode) ? evt.charCode : ((evt.keyCode) ? evt.keyCode : ((evt.which) ? evt.which : 0));
    if (key == "13") {
      var cid, leftstr, tblid, CallFn;
      cid = this.getAttribute("id");
      leftstr = this.getAttribute("id").split("_")[0];
      tblid = cid.substring(leftstr.length+1,cid.length);
      for (var i in ModFn) {
        ModFnId[i] == tblid ? CallFn=true : CallFn=false;
      }
      (CallFn) ? ModFn[i].call() : Filter(tblid);
    }//if key
  }//if evt
}

function swapOddEven(row, orderNr) {
/*====================================================
  - swaps style from odd to even depending on order number,
   do provide the real order number as in "visible"...
=====================================================*/
  if ((orderNr % 2) == 0) {
    if ((row.className.indexOf('odd') > -1)) {
      row.className = replace(row.className, 'odd', 'even');
    } else {
      row.className = row.className.indexOf("even") > -1 ? row.className : row.className + " even";
    }
  } else {
    if ((row.className.indexOf('even') > -1)) {
      row.className = replace(row.className, 'even', 'odd');
    }
    row.className = row.className.indexOf("odd") > -1 ? row.className : row.className + " odd";
  }
}

// #############################  Sort functions ############################

function ts_sort_date(a, b) {
  // y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
  var aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
  var bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
  var dt1, yr, dt2;
  if (aa.length == 10) {
    dt1 = aa.substr(6, 4) + aa.substr(3, 2) + aa.substr(0, 2);
  } else {
    yr = aa.substr(6, 2);
    if (parseInt(yr) < 50) {
      yr = '20' + yr;
    } else {
      yr = '19' + yr;
    }
    dt1 = yr + aa.substr(3, 2) + aa.substr(0, 2);
  }
  if (bb.length == 10) {
    dt2 = bb.substr(6, 4) + bb.substr(3, 2) + bb.substr(0, 2);
  } else {
    yr = bb.substr(6, 2);
    if (parseInt(yr) < 50) {
      yr = '20' + yr;
    } else {
      yr = '19' + yr;
    }
    dt2 = yr + bb.substr(3, 2) + bb.substr(0, 2);
  }
  if (dt1 == dt2) {
    return 0;
  }
  if (dt1 < dt2) {
    return -1;
  }
  return 1;
}

function ts_sort_currency(a, b) {
  var aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
  var bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
  return isNaN(parseFloat(aa) - parseFloat(bb)) ? -1 : parseFloat(aa) - parseFloat(bb) ;
  // if the value isNaN we return -1 else the last sortable number isn't sorted correctly
}


function ts_sort_numeric(a, b) {
  var aa = parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
  var bb = parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));

  if (isNaN(aa)) {
    aa = 0;
  }
  if (isNaN(bb)) {
    bb = 0;
  }
  return aa - bb;
}

function ts_sort_caseinsensitive(a, b) {
  var aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
  var bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();

  if (aa == bb) {
    return 0;
  }
  if (aa < bb) {
    return -1;
  }
  return 1;
}

function ts_sort_default(a, b) {
  var aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
  var bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
  if (aa == bb) {
    return 0;
  }
  if (aa < bb) {
    return -1;
  }
  return 1;
}

The style sheet extension code is:

/* $xwiki.ssfx.use("js/xwiki/table/table.css") */
@import "/resources/js/xwiki/table/table.css";

The (german) content of the article itself (as a tiny manual) is:

Code für die sortier- und filterbare Tabelle.

== Beispiel ==
(% class="grid sortable filterable doOddEven" id="tableid" %)
(% class="sortHeader" %)|=Name|=Vorname
|Alice|Wunderland
|Bob|der Baumeister
|Carola|Rakete
|David|Bowie

== Benutzung ==
Um eine Tabelle filter- und sortierbar zu haben, müssen von Hand 2 Zeilen Klassen im Sourcecode mit dem Wiki-Editor eingefügt werden. Die Klasse "sortHeader" (2. Zeile) muss ohne Leerzeichen direkt vor dem Tabellen-Header stehen.

Die zwei Zeilen Klassen zum Einfügen lauten:

{{code language="java"}}
(% class="grid sortable filterable doOddEven" id="tableid" %)
(% class="sortHeader" %)
{{/code}}

== Beispiel ==
Der gesamte Code der Tabelle oben sieht dann so aus:

{{code language="java"}}
(% class="grid sortable filterable doOddEven" id="tableid" %)
(% class="sortHeader" %)|=Name|=Vorname
|Alice|Wunderland
|Bob|der Baumeister
|Carola|Rakete
|David|Bowie
{{/code}}

{{warning}}
Achtung: Sollten mehrere Tabellen innerhalb eines Artikels mit diesen Eigenschaften versehen werden, muss die //id// eineindeutig vergeben werden. Sie darf nicht mehrfach verwendet werden.
{{/warning}}

== Beispiel 2 ==

(% class="grid table-hover" id="tableid3" style="width:auto" %)
|=Überschrift 1|=Überschrift 2
|Zelle Zeile 1 Spalte 1|Zelle Zeile 1 Spalte 2
(% class="info" %)|Zelle Zeile 2 Spalte 1|Zelle Zeile 2 Spalte 2
(% class="success" %)|Zelle Zeile 3 Spalte 1|Zelle Zeile 3 Spalte 2
(% class="warning" %)|Zelle Zeile 4 Spalte 1|Zelle Zeile 4 Spalte 2
(% class="danger" %)|Zelle Zeile 5 Spalte 1|Zelle Zeile 5 Spalte 2
(% class="active" %)|Zelle Zeile 6 Spalte 1|Zelle Zeile 6 Spalte 2
|Zelle Zeile 7 Spalte 1|Zelle Zeile 7 Spalte 2
|Zelle Zeile 8 Spalte 1|(% class="info" %)Zelle Zeile 8 Spalte 2

Die Tabelle oben drüber muss z.B. so aussehen:

{{code language="none"}}
(% class="grid table-hover" id="tableid3" style="width:auto" %)
|=Überschrift 1|=Überschrift 2
|Zelle Zeile 1 Spalte 1|Zelle Zeile 1 Spalte 2
(% class="info" %)|Zelle Zeile 2 Spalte 1|Zelle Zeile 2 Spalte 2
(% class="success" %)|Zelle Zeile 3 Spalte 1|Zelle Zeile 3 Spalte 2
(% class="warning" %)|Zelle Zeile 4 Spalte 1|Zelle Zeile 4 Spalte 2
(% class="danger" %)|Zelle Zeile 5 Spalte 1|Zelle Zeile 5 Spalte 2
(% class="active" %)|Zelle Zeile 6 Spalte 1|Zelle Zeile 6 Spalte 2
|Zelle Zeile 7 Spalte 1|Zelle Zeile 7 Spalte 2
|Zelle Zeile 8 Spalte 1|(% class="info" %)Zelle Zeile 8 Spalte 2
{{/code}}

----

== Hinweise ==

* Note that ##~(% class="sortHeader" %)## must be placed directly before the first header so that the table can be sorted according to the table columns.
* Without column headers, the ##~(% class="sortHeader" %)## must be omitted.
* If there are several tables in an article, make sure that each table has its own unique ##id##.
* If you only want to filter without sorting, leave out the ##sortable##.
* Sorting without filtering is also possible if you omit the ##filterable##.
* The classes ##doOddEven## and ##table-striped## basically do the same. However, the class ##doOddEven## does not work alone (or together with ##grid##) and the class ##table-striped## creates the stripes in the wrong order when the class ##filterable## is needed.
* Tables that are only as wide as necessary (instead of the full article width) require the ##style="width: auto; "## after the classes.
* Table rows or columns can be marked with predefined colors. To do this, enter the corresponding class before the entire row or at the front of the corresponding cell (see 2nd example, row 8)
* The predefined colors can be found in example 2 with the following classes:
** ##info##: blue like row 2
** ##success##: green like row 3
** ##warning##: yellow like line 4
** ##danger##: red like line 5
** ##active##: gray like line 6
* Tables whose line hovered over with the mouse should have a gray frame require the class ##table-hover##, as in example 3.

That part with all the importent notes I translated in english. Note that some classes like “table-striped” or “table-hover” came from xwikis bootstrap. They are not documented by the java script extension code above.

So I think you must get rid of the macro and then you can implement it like described above.
SImpel

1 Like

Thanks, @Simpel! It will take me some time to understand and use correctly the whole thing. I will also keep trying to understand the oddities of using the macro: it will for sure help me to remember many XWiki features. And perhaps others if I find any don’t expected behaviour.

I will report back to this thread ASAP!

Hi @Simpel, all! I would like to maintain and try to improve the document @Simpel and mates created about filterable and sortable tables. @Simpel, could you share the Notes in German? I will be happy to create an account for you on our XWiki instance if you prefer to edit it online! The page is currently here:

https://portal.igfae.usc.es/xwiki/bin/view/Table/

I’m also struggling to find Example 3! It is in the last bulleted point under Notes. To which example do you refer?

Thanks!

Of course:

== Hinweise ==

* Beachte, dass ##~(% class="sortHeader" %)## direkt vor der ersten Überschrift stehen muss, damit die Tabelle nach den Tabellenspalten sortierbar ist.
* Ohne Spalten-Überschriften muss das ##~(% class="sortHeader" %)## weg gelassen werden.
* Sollten mehrere Tabellen in einem Artikel vorhanden sein, muss darauf geachtet werden, dass jede Tabelle eine eigene, eineindeutige ##id## erhält.
* Wenn man nur filtern ohne sortieren möchte dann lässt man das ##sortable## weg.
* Auch sortieren ohne filtern geht, wenn man das ##filterable## weglässt.
* Die Klassen ##doOddEven## und ##table-striped## tun im Prinzip dasselbe. Jedoch funktioniert die Klasse ##doOddEven## nicht alleine (oder mit ##grid## zusammen) und die Klasse ##table-striped## erzeugt die Streifen in der falschen Reihenfolge wenn die Klasse ##filterable## benötigt wird.
* Tabellen, die nur so breit wie nötig sind (statt der vollen Artikelbreite) benötigen nach den Klassen noch den ##style="width: auto;"##.
* Tabellenzeilen oder -spalten können mit vorgegebenen Farben markiert werden. Dazu die entsprechende Klasse vor die gesamte Zeile oder vorne in die entsprechende Zelle (siehe 2. Beispiel Zeile 8) eintragen
* Die vordefinierten Farben finden sich im Beispiel 2 mit folgenden Klassen:
** ##info##: blau wie Zeile 2
** ##success##: grün wie Zeile 3
** ##warning##: gelb wie Zeile 4
** ##danger##: rot wie Zeile 5
** ##active##: grau wie Zeile 6
* Tabellen, deren mit der Maus überfahrene Zeile (hover) einen grauen Rahmen bekommen soll, benötigen die Klasse ##table-hover##, wie in Beispiel 2.

Sobald die erwünschten Klassen eingetragen sind klickt man wieder den Knopf "Quellcode", um wieder in den WYSIWYG-Editor zurück zu kommen.

{{warning}}
Achtung: Leider werden nicht alle eingetragenen Klassen der Tabelle sofort im inline WYSIWYG-Editor angezeigt. Nach dem Speichern des Artikels muss man die Seite einmal neu laden (z.B. mit "F5"), damit alle Klassen wirken. Dieses Verhalten zeigt sich aber nur während der Bearbeitung eines Artikels.
{{/warning}}

A mistake of mine. Our original had 3 examples but the first one does not fit and I had to adapt the text and forgot the last one.

PS: The last warning box I completely forgot in my post before.

@Simpel Thanks! I will work on it:

https://portal.igfae.usc.es/xwiki/bin/view/Table/

I’ve been not able yet to create data sources pointing to remote databases (not the one where XWiki data are stored) to feed Live Data Macro. I’m using the objects you proposed and Make Tables Sortable Macro in my way to fully understand Live Data Macro.

Any idea or comment is extremely welcome! Thanks!