Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idea: Presenters as inverse of parsers #812

Closed
VorontsovIE opened this issue Feb 4, 2015 · 10 comments
Closed

Idea: Presenters as inverse of parsers #812

VorontsovIE opened this issue Feb 4, 2015 · 10 comments

Comments

@VorontsovIE
Copy link
Collaborator

Hello, @Mottie
In our site we have rather big table, with lots of uniform links (such as external search links) and other markup stuff in text. Not to overload internet channel we decided to respond with plain-text table and to apply necessary markup with javacript after page was loaded. Not to make double work we apply markup after tablesort filtering and sorting procedures, so that we do not need parsers.
I think it can be useful to make presenters which behave similar to parsers/extractors but perform just the opposite thing: changes text to be rendered (while keeping text to be parsed untouched). I hope that it will be possible to write something like:

<table>
<thead>
<tr>
<th class="presenter-bold">Gene name</th>
<th class="wiki">Gene wikipedia</th>
<th class="genenames">Genenames</th>
<th class="emphasize">Associated disease</th>
<th class="wiki">Disease wikipedia</th>
</tr>
</thead>
<tbody>
<tr>
<td>P53</td>
<td>P53</td>
<td>TP53</td>
<td>Li–Fraumeni_syndrome</td>
<td>Li–Fraumeni_syndrome</td>
</tr>
</tbody>
</table

With following tablesorter configuration

$.tablesorter.addPresenter({
  id: 'bold',
  is: is: function(s, table, cell, $cell) { return false; },
  format: function(text, table, cell, cellIndex) {
    return '<b>' + text + '</b>'
  }
  // flag for filter widget (true = search presented values; false = search original text)
  presented: false,
  type: 'text'
});

$.tablesorter.addPresenter({
  id: 'link',
  is: is: function(s, table, cell, $cell) { return false; },
  format: function(text, table, cell, cellIndex) {
    var c = table.config,
        ci = c.$headers.filter('[data-column=' + cellIndex + ']:last'),
        presenterConfig = ci.length && ci[0].presenterLinkOptions || $.tablesorter.getData( ci, ts.getColumnData( table, c.headers, cellIndex ), 'presenter-link-options'),
        link_conv = presenterConfig.link_converter || function(text, table, cell, cellIndex){ return '#'; },
        text_conv = presenterConfig.text_converter || function(text, table, cell, cellIndex){ return text; };
    return '<a href="' + link_conv(text, table, cell, cellIndex) + '">' + text_conv(text, table, cell, cellIndex) '</a>'
  }
  // flag for filter widget (true = search presented values; false = search original text)
  presented: false,
  type: 'text'
});


$(function(){
  $("table").tablesorter({
    headers: {
      '.emphasize': {presenter: 'bold'},

      '.wiki': {
        presenter: 'link',
        presenterLinkOptions: {
          text_converter: function() { return 'wiki'; }
          link_converter: function(text, table, cell, cellIndex) { return 'en.wikipedia.org/wiki/' + text; };
        }
      },

      '.genenames': { presenter: 'link',
        presenterLinkOptions: {
          text_converter: function(text) { return text; }
          link_converter: function(text, table, cell, cellIndex) { return 'http://www.genenames.org/cgi-bin/search?search_type=symbols&search=' + text + '&submit=Submit'; };
        }
      }
    }
  });
});

And I expect that following html will be rendered:

<table>
<thead>
<tr>
<th class="presenter-bold">Gene name</th>
<th class="wiki">Gene wikipedia</th>
<th class="genenames">Genenames</th>
<th class="emphasize">Associated disease</th>
<th class="wiki">Disease wikipedia</th>
</tr>
</thead>
<tbody>
<tr>
<td><b>P53</b></td>
<td><a href="en.wikipedia.org/wiki/P53">wiki</a></td>
<td><a href="http://www.genenames.org/cgi-bin/search?search_type=symbols&search=TP53&submit=Submit">TP53</a></td>
<td><b>Li–Fraumeni_syndrome</b></td>
<td><a href="en.wikipedia.org/wiki/Li–Fraumeni_syndrome">wiki</a></td>
</tr>
</tbody>
</table

Another example: using presenters it'll be possible to take an html with plain-text grades and generate selectors like in http://mottie.github.io/tablesorter/docs/example-extractors-parsers.html, not to use extractors and not to send generate lots of markup on server.

I propose to implement presenters in a way similar to parsers and extractors. If you agree with proposed feature, I will try to implement it. The only big problem I see is in implementing table caching. I'd appreciate any advices and suggestions on how to better implement it or any other feedback about why I should or shouldn't implement such a feature.

@Mottie
Copy link
Owner

Mottie commented Feb 4, 2015

Hi @prijutme4ty!

That is a very interesting idea. I think though that doing something like this can also be accomplished with a widget. I put together this demo using the following widget:

var ts = $.tablesorter;

ts.formatter = {
    init : function( c ) {
        c.$table.on( 'updateComplete.tsformatter', function() {
            ts.formatter.setup( c );
        });
        ts.formatter.setup( c );
    },
    setup : function( c ) {
        // do nothing for empty tables
        if ( $.isEmptyObject( c.cache ) ) { return; }
        var tbodyIndex, rowIndex, rows, len, $row, formatter,
            wo = c.widgetOptions,
            data = { config: c, wo: wo };
        for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ){
            rows = c.cache[ tbodyIndex ];
            len = rows.normalized.length;
            for ( rowIndex = 0; rowIndex < len; rowIndex++ ) {
                data.$row = rows.normalized[ rowIndex ][ c.columns ].$row;
                for ( column = 0; column < c.columns; column++ ) {
                    formatter = ts.getColumnData( c.table, wo.formatter_column, column, true );
                    if ( typeof formatter === 'function' ) {
                        data.columnIndex = column;
                        data.$header = c.$headers.filter('[data-column="' + column + '"]:last');
                        data.$cell = data.$row.children( 'th, td' ).eq( column );
                        data.text = data.$cell.text();
                        data.$cell.html( formatter( data.text, data ) );
                    }
                }
            }
        }
    }
};

ts.addWidget({
    id: 'formatter',
    priority: 100,
    options: {
        formatter_column: {}
    },
    init: function (table) {
        ts.formatter.init( table.config );
    }
});

Then when you use the widget, you can add functions to process text from each column:

$(function () {
    $('table').tablesorter({
        theme: 'blue',
        widgets: ['formatter'],
        widgetOptions: {
            formatter_column: {
                '.emphasize' : function( text, data ) {
                    return '<strong>' + text + '</strong>';
                },
                '.wiki' : function( text, data ) {
                    if ( text === '' ) { return ''; }
                    // add text to data-attribute; this overrides the parser
                    data.$cell.attr( data.config.textAttribute, text );
                    // replace table cell with a link
                    return '<a href="en.wikipedia.org/wiki/' + text + '">wiki</a>';
                },
                '.genenames' : function( text, data ) {
                    if ( text === '' ) { return ''; }
                    data.$cell.attr( data.config.textAttribute, text );
                    return '<a href="http://www.genenames.org/cgi-bin/search?search_type=symbols&search=' + text + '&submit=Submit">' + text + '</a>';
                }
            }
        }
    });
});

The data parameter in the formatter_column function includes the following:

data = {
  config: table.config,
  wo : table.config.widgetOptions,
  $header : $('th'), // the header cell of the current column
  $row : $('tr'), // jQuery object of the row currently being processed
  $cell : $('td'), // jQuery object of the cell currently being processed
  text:  'Fred', // the text from the current cell
  columnIndex: 0 // the column index of the current cell
}

Also, in the formatter funciton, if you want to change the text within the cell, save the original text into the data-attribute, like this:

data.$cell.attr( data.config.textAttribute, text );

Doing this will keep the original text in the parser and allow you to modify the displayed text in any way you desire.

I'll add this widget to the repository and it will be available in the next update.

@VorontsovIE
Copy link
Collaborator Author

Wow! Thank you, @Mottie!
I initially thought about widget but didn't find an easy way to access infos attached to a header (especially in a convenient way: by classname as new tablesorter does). Your method is definitely much, much better than my proposal. Will try it with our tables right now :)

@VorontsovIE
Copy link
Collaborator Author

@Mottie, is there any way to configure filters to look at textAttribute as sorters do?

@Mottie
Copy link
Owner

Mottie commented Feb 6, 2015

It already does!

I was going to tell you to get the updated formatter widget from the repository instead of using the code above: widget-formatter.js

The update has a lot of optimizations and fixes.

If you look at line 48 you'll see this code:

data.text = cell.getAttribute( c.textAttribute ) || cell.textContent || data.$cell.text();

so the text is preferentially taken from the textAttribute option setting, then cell text.

@VorontsovIE
Copy link
Collaborator Author

I've checked it out but it didn't help me. If you set for example filter-select on "image links" column in an example, it will give you the only value "Search The Duck". It can be overriden by setting filter_useParsedData: true, but I'm not sure it's the right way.

@Mottie
Copy link
Owner

Mottie commented Feb 6, 2015

Oh, nice catch... that actually looks like a bug in the filter widget. I'll have that fixed here in a second.

@VorontsovIE
Copy link
Collaborator Author

Got it. Now it works like a charm! Thank you again.

@Mottie
Copy link
Owner

Mottie commented Feb 7, 2015

Oops, I missed two more spots. It should work now without setting the filter to use parsed data.

@VorontsovIE
Copy link
Collaborator Author

M! Not only toy example, but my own table with lots of widgets turned on now works too. 👍
Are there any obstacles (in compatibility or speed) to make a refactoring and extract an uniform interface which can be used to get text extracting in the only place?

@Mottie
Copy link
Owner

Mottie commented Feb 7, 2015

I've been thinking about doing that - have a module which centralizes the code used to gather column data; but I think I just need to add a non-parsed value along with a parsed value for each cell in the cache. If that was done, the filter widget would not need to extract the text again and probably speed it up even more.

There is a problem with centralizing text extraction though... because each widget would need to do something a little different to a column. For example:

  • The formatter widget cycles through every row in the table just once. While processing a row, it figures out which columns need to me modified. This is much more efficient than cycling through every row for the first modified column, then every row for the second modified column, etc.
  • The output widget is a little different in that it actually takes colspan and rowspan within the tbody into account.
  • The filter select option code only grabs the text for one column. If there is more than one column that needs a select, it cycles through each one independently -maybe this could be made more efficient by following the same pattern as the other widgets.

I'll have to look into this more when I have more time, as it will require changes to numerous widgets.

@Mottie Mottie closed this as completed in df87314 Feb 7, 2015
@Mottie Mottie removed the Next Update label Feb 7, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants