Search code examples
javascriptcontenteditabletablesorter

Tablesorter always reloads first page after content-edit


I have an application with a tablesorter implementation. It features content-editable, filtering and ajax server pager. All is working fine, except for a single issue: every time a user submit an edited value while on a page > 1, the content is submitted but tablesorter reloads the first page.

Looking at the xhr request it is clear that a new get request is made (behind the scenes I guess, in my code I don't trigger any update or reload). It is good to me anyway, it is fine to refresh content, but I'd like to keep the page where the user was before edit.

As I can see, the issue is occurring on page parameter only: if I change the number of elements per page, put filters in columns, or order one or more fields, tablesorter keeps track of them, and they are correctly updated in the request url parameters, except for page parameter, that is always reverted back to 0.

Example of this on a progression viewed from xhr requests side:

First request (default): vlansummarys?page=0&size=10&col[10]=0&col[8]=0&col[7]=0&fcol

Filter added: vlansummarys?page=0&size=10&col[10]=0&col[8]=0&col[7]=0&fcol[3]=monocos

Page elements from 10 to 20: vlansummarys?page=0&size=20&col[10]=0&col[8]=0&col[7]=0&fcol[3]=monocos

New ordering field added: vlansummarys?page=0&size=20&col[10]=0&col[8]=0&col[7]=0&col[3]=0&fcol[3]=monocos

Page changed to 2: vlansummarys?page=1&size=20&col[10]=0&col[8]=0&col[7]=0&col[3]=0&fcol[3]=monocos

Content editing, call made after it: vlansummarys?page=0&size=20&col[10]=0&col[8]=0&col[7]=0&col[3]=0&fcol[3]=monocos

EDIT

As requested I'm posting my code. Since it is really hard to reduce it down to a simple working example, I will paste all my tablesorter initialization function. It can't work alone since it relies on other data and variables calculated on the page, but I guess it should give a pretty good idea. Please ask me any further detail that could be of help.

function LoadTable() {
    $('#table').tablesorter({
        theme: 'bootstrap',
        //widthFixed: true,
        zebra: [
            "even",
            "odd"],
        dateFormat: "ddmmyyyy",
        headerTemplate: '{content}',
        sortList: [[10, 0], [8, 0], [7, 0]], //Order by AdR del Kit ASC, AdR ASC, Centrale ASC
        initWidgets: true,
        widgets: bReadOnly ? ['zebra', 'columns', 'filter', 'uitheme'] : ['zebra', 'columns', 'filter', 'uitheme', 'editable'],
        widgetOptions: {
            filter_columnFilters: true,
            filter_cssFilter: arrHeaderFields,
            editable_columns: (bReadOnly ? null : [21]),       // or "0-2" (v2.14.2); point to the columns to make editable (zero-based index)
            editable_enterToAccept: true,          // press enter to accept content, or click outside if false
            editable_autoAccept: false,          // accepts any changes made to the table cell automatically (v2.17.6)
            editable_autoResort: false,         // auto resort after the content has changed.
            editable_noEdit: 'no-edit',     // class name of cell that is not editable
            editable_editComplete: 'editComplete', // event fired after the table content has been edited
            editable_validate: null,          // return a valid string: function(text, original){ return text; }
            editable_focused: null,/*function (txt, columnIndex, $element) {
                // $element is the div, not the td
                // to get the td, use $element.closest('td')
                //$element.addClass('focused');
                $element.removeClass("emptyPlaceholder");
                //SelectActivationDateText($element.closest('td'));
            },*/
            editable_blur: null,/*function (txt, columnIndex, $element) {
                // $element is the div, not the td
                // to get the td, use $element.closest('td')
                //$element.removeClass('focused');
                RestoreCellStyle($element);
            },*/
            editable_selectAll: null,/*true,function (txt, columnIndex, $element) {
                // note $element is the div inside of the table cell, so use $element.closest('td') to get the cell
                // only select everthing within the element when the content starts with the letter "B"
                //return /^b/i.test(txt) && columnIndex === 0;
            },*/
            editable_wrapContent: null,//'<div>',       // wrap all editable cell content... makes this widget work in IE, and with autocomplete
            /*reorder_axis: 'x', // 'x' or 'xy'
            reorder_delay: 300,
            reorder_helperClass: 'tablesorter-reorder-helper',
            reorder_helperBar: 'tablesorter-reorder-helper-bar',
            reorder_noReorder: 'reorder-false',
            reorder_blocked: 'reorder-block-left reorder-block-end',
            reorder_complete: null // callback*/
        },
    }).tablesorterPager({
        // target the pager markup - see the HTML block below
        container: $(".pager"),

        // use this url format "http:/mydatabase.com?page={page}&size={size}" 
        ajaxUrl: "/application/vlansummarys?page={page}&size={size}&{sortList:col}&{filterList:fcol}",

        // modify the url after all processing has been applied
        customAjaxUrl: function(table, url) { return url; },

        ajaxProcessing: function (data) {
            if (data && data.hasOwnProperty('rows')) {
                var str = "", d = data.rows,
                    // total number of rows (required)
                    total = data.total_rows,
                    // len should match pager set size (c.size)
                    len = d.length;

                for (var i = 0; i < len; i++) {
                    str += '<tr>';
                    for (var column = 0; column < orderedFieldMapping.length; column++) {
                        //Distinzione temporanea per gestire i casi di dato non presente (Data Attivazione) e handler di selezione
                        if (orderedFieldMapping[column].toUpperCase() != 'ACTIVATIONDATE' || bReadOnly)
                            str += '<td class="' + orderedFieldMapping[column].toUpperCase() + '"' + ($('#' + orderedFieldMapping[column].toUpperCase()).prop('checked') ? '' : 'style="display:none;"') + '><div>' + (eval('d[i].' + orderedFieldMapping[column]) != null ? eval('d[i].' + orderedFieldMapping[column]) : '') + '</div></td>';
                        else
                            str += '<td title="Inserire la data nel formato gg/mm/aaaa (click per inserimento)" class="' + orderedFieldMapping[column].toUpperCase() + '"' + ($('#' + orderedFieldMapping[column].toUpperCase()).prop('checked') ? '' : 'style="display:none;"') + '><div contenteditable="true" ' + (eval('d[i].' + orderedFieldMapping[column]) != null ? '' : 'class="emptyPlaceholder" ') + 'onmouseup="javascript:SelectActivationDateText(this);" onblur="javascript:RestoreCellStyle(this);">' + (eval('d[i].' + orderedFieldMapping[column]) != null ? eval('d[i].' + orderedFieldMapping[column]) : emptyTextString) + '</div></td>';
                    }
                    str += '</tr>';
                }

                // in version 2.10, you can optionally return $(rows) a set of table rows within a jQuery object
                return [total, $(str)];
            }
        },

        ajaxObject: {
            dataType: 'json'
        },

        // output string - default is '{page}/{totalPages}';
        // possible variables:
        // {page}, {totalPages}, {startRow}, {endRow} and {totalRows}
        output: '{startRow} to {endRow} ({totalRows})',

        // apply disabled classname to the pager arrows when the rows at
        // either extreme is visible - default is true
        updateArrows: true,

        // starting page of the pager (zero based index)
        page: 0,

        // Number of visible rows - default is 10
        size: 20,

        //Reset pager to this page after filtering; set to desired page number (zero-based index), or false to not change page at filter start (Updated v2.16). 
        pageReset: false,

        // if true, the table will remain the same height no matter how many
        // records are displayed. The space is made up by an empty 
        // table row set to a height to compensate; default is false 
        fixedHeight: true,

        // remove rows from the table to speed up the sort of large tables.
        // setting this to false, only hides the non-visible rows; needed
        // if you plan to add/remove rows with the pager enabled.
        removeRows: false,

        // css class names of pager arrows
        // next page arrow
        cssNext: '.next',
        // previous page arrow
        cssPrev: '.prev',
        // go to first page arrow
        cssFirst: '.first',
        // go to last page arrow
        cssLast: '.last',
        // select dropdown to allow choosing a page
        cssGoto: '.gotoPage',
        // location of where the "output" is displayed
        cssPageDisplay: '.pagedisplay',
        // dropdown that sets the "size" option
        cssPageSize: '.pagesize',
        // class added to arrows when at the extremes 
        // (i.e. prev/first arrows are "disabled" when on the first page)
        // Note there is no period "." in front of this class name
        cssDisabled: 'disabled'
    }).children('tbody').on('editComplete', 'td', function() {
        var $this = $(this),
            //$allRows = $this.closest('table')[0].config.$tbodies.children('tr'),
            newContent = $this.text(),
            /*cellIndex = this.cellIndex, // there shouldn't be any colspans in the tbody
            rowIndex = $allRows.index($this.closest('tr')),*/
            id = $this.closest('tr').find('td.ID').text();

        $.ajax({
            type: "POST",
            crossDomain: true,
            url: '/application/vlansummarys/update/' + id,
            data: JSON.stringify({ activationDate: newContent }),
            dataType: "text",
            contentType: "application/json; charset=utf-8",
            error: function(xhr, textStatus, errorThrown) {
                alert("Errore durante l'inserimento: il dato inserito non è corretto. Contattare sistemi informativi se si ritiene che il messaggio d'errore sia sbagliato.");
            },
            success: function(data, textStatus, xhr) {
                //console.log(xhr);
                if ($this.find('div').hasClass('emptyPlaceholder'))
                    $this.find('div').removeClass('emptyPlaceholder');
            },
        });
    });
}

Solution

  • So the problem turned out to be an issue with the pager addon/widget. When a cell is edited, an "updateCell" event is triggered by the editable widget to update the internal cache. Once this process completes, an "updateComplete" event is triggered by the core plugin.

    In the pager code, when an "updateComplete" event is triggered, it updates the total rows and total pages inappropriately because ajax is being used and it only counts the rows currently in the table.

    So basically, I added a check to prevent recalculating the page/row count if ajax is being used.

    Get this latest update for the pager from the GitHub repository master branch.