Widget:Filter table

Description
This widget provides filter buttons above tables, and will allow table content to be filtered by CSS classes applied to rows.

Parameters

 * tableid
 * The "id" attribute of the table to target with the widget


 * filters
 * A comma separated list of classes to filter for.
 * Optional: Use a broken-pipe symbol to display a different button text to the underlying value, e.g.
 * Optional: Use wikicode in the button display text, including icons - sorry no commas.
 * Optional: A value of  will instead insert a newline.
 * Optional: A value of label¦displayname will instead insert a label.


 * mode
 * Optional. Defaults to "AND", which uses AND logic to connect filters.
 * Can be set to "OR" to use OR logic to join the filter buttons with OR logic instead.
 * Can also be set to "ONLY" logic to only allow one active filter at a time.

Example

 * AND


 * OR


 * ONLY

/* Widget: Filter table CSS */ .filter-controls-wrapper { margin: 10px 0 0; display: table; } .filter-controls-wrapper fieldset { margin: 0 !important; } .filter-controls-wrapper p { margin: 0px; } ul.filter-table-controls { margin: 0px; } .filter-table-clear, .filter-table-button { margin: 3px; padding: 2px 5px; } .filter-table-button.filter-active { background-color: #E5F1FB; border: 1px solid #0078D7; padding: 3px 6px 3px 6px; margin: 3px 4px; } .filter-table-label { display:inline-block; margin: 3px; padding: 2px 5px; } /* Widget: Filter table JS */ // DEFER LOADING SCRIPT UNTIL JQUERY IS READY. WAIT 40MS BETWEEN ATTEMPTS. function defer(method) { if (window.jQuery && mw && mw.loader && mw.loader.using) { method; } else { setTimeout(function { defer(method) }, 40); } }

// INITIALISATION defer(function {

// Collect widget inputs var table_id = ''; var filter_classes = ''; filter_classes = filter_classes.split(','); var mode = ''; mode = mode.toLowerCase;

// Add table specific stylesheet, with unique id so it can be overwritten var style = document.createElement('style'); style.id = table_id + '-css'; $('head').append(style);

// Initialise filters by adding new rules (setting the "display: none" rule here too in case JS is disabled, which would obstruct tables with noscript extensions) $('#' + table_id + '-css').text('#' + table_id + ' .filter-row { display: none; }' + '\n'                              + '#' + table_id + ' .filter-row { display: table-row }'                               );

// Sanitise inputs filter_classes = $.map(filter_classes, function(v) {   var o = {};    var a = v.split('¦');    if (a.length == 1) {        if (a[0].trim === ' ') {            o.type = 'br';        } else {            o.type = 'button';            o.css_class = a[0].trim;            o.text = a[0].trim;        }    } else if (a.length == 2) {        if (a[0].trim === 'label') {            o.type = 'label';            o.text = a[1].trim;        } else {            o.type = 'button';            o.css_class = a[0].trim;            o.text = a[1].trim;        }    }    return o; }); if (mode != 'or' && mode != 'and' && mode != 'only') { console.log('Invalid mode operator, setting to AND.'); mode = 'and'; }

// Specify text label var filter_text = 'The table below can be filtered using the following buttons.' if (mode != 'only') { filter_text += ' Multiple ' + mode.toUpperCase + ' filters can be applied at once by clicking on multiple buttons.'; }

// Build filter controls var control_wrapper_div = document.createElement('div'); control_wrapper_div.className = 'filter-controls-wrapper';

var fieldset = document.createElement('fieldset'); fieldset.id = table_id + '-controls';

var legend = document.createElement('legend'); legend.innerText = 'Table filter options:';

fieldset.appendChild(legend);

var p = document.createElement('p'); p.style['margin-bottom'] = '0.5em'; p.innerText = filter_text;

fieldset.appendChild(p);

var ul = document.createElement('ul'); ul.className = 'filter-table-controls';

var button = document.createElement('button'); button.classList.add('filter-table-clear'); button.classList.add('mw-ui-button'); button.innerText = 'Show all';

ul.appendChild(button);

var text_id = 0; $.each(filter_classes, function(i, v) {   if (v.type === 'br') {        var br = document.createElement('br');        ul.appendChild(br);    } else if (v.type === 'button') {        var button = document.createElement('button');        button.classList.add('filter-table-button');        button.classList.add('mw-ui-button');        button.setAttribute('data-value', v.css_class.replace(/ /g,'-'));        button.setAttribute('data-textargettid', text_id);        text_id++;        button.innerText = v.text;        ul.appendChild(button);    } else if (v.type === 'label') {        var div = document.createElement('div');        div.className = 'filter-table-label';        div.setAttribute('data-textargettid', text_id);        text_id++;        div.innerText = v.text;        ul.appendChild(div);    } });

fieldset.appendChild(ul);

control_wrapper_div.appendChild(fieldset);

$('#' + table_id).before(control_wrapper_div);

// Run each filter button and label text through the API to see if there were icons or wikilinks etc var parse_payload = 'WIDGETSEPARATOR' + $.map(filter_classes, function(v){   return v.text; }).join('WIDGETSEPARATOR') + 'WIDGETSEPARATOR'; mw.loader.using('mediawiki.api', function {    var api = new mw.Api;    api.parse(parse_payload)    .done(function (result) { var parsed_payload = result.split('WIDGETSEPARATOR');

// Remove first two elements where the header and footer of the parsed data will be (div open, div closed+pp limit report) parsed_payload.pop; parsed_payload.shift;

// Distribute headers back to source $.map( $('#' + table_id + '-controls' + ' ' + '.filter-table-button'), function(v){           var num = v.attributes['data-textargettid'].value;            $(v).html(parsed_payload[num]);        }); $.map( $('#' + table_id + '-controls' + ' ' + '.filter-table-label'), function(v){           var num = v.attributes['data-textargettid'].value;            $(v).html(parsed_payload[num]);        }); }); });

// Reused core MW code: https://wiki.guildwars2.com/resources/src/jquery/jquery.tablesorter.js // Replace all rowspanned cells in the table body with clones in each row function explodeRowspans( $table ) { var spanningRealCellIndex, rowSpan, colSpan, cell, cellData, i, $tds, $clone, $nextRows, rowspanCells = $table.find( '> tbody > tr > [rowspan]' ).get;

// Short circuit if ( !rowspanCells.length ) { return; }

// First, we need to make a property like cellIndex but taking into // account colspans. We also cache the rowIndex to avoid having to take // cell.parentNode.rowIndex in the sorting function below. $table.find( '> tbody > tr' ).each( function {        var i,            col = 0,            len = this.cells.length;        for ( i = 0; i < len; i++ ) {            $( this.cells[ i ] ).data( 'tablesorter', { realCellIndex: col, realRowIndex: this.rowIndex });           col += this.cells[ i ].colSpan;        }    });

// Split multi row cells into multiple cells with the same content. // Sort by column then row index to avoid problems with odd table structures. // Re-sort whenever a rowspanned cell's realCellIndex is changed, because it   // might change the sort order. function resortCells { var cellAData, cellBData, ret; rowspanCells = rowspanCells.sort( function ( a, b ) {           cellAData = $.data( a, 'tablesorter' );            cellBData = $.data( b, 'tablesorter' );            ret = cellAData.realCellIndex - cellBData.realCellIndex;            if ( !ret ) {                ret = cellAData.realRowIndex - cellBData.realRowIndex;            }            return ret;        }); rowspanCells.forEach( function ( cell ) {           $.data( cell, 'tablesorter' ).needResort = false;        }); }   resortCells;

function filterfunc { return $.data( this, 'tablesorter' ).realCellIndex >= spanningRealCellIndex; }

function fixTdCellIndex { $.data( this, 'tablesorter' ).realCellIndex += colSpan; if ( this.rowSpan > 1 ) { $.data( this, 'tablesorter' ).needResort = true; }   }

while ( rowspanCells.length ) { if ( $.data( rowspanCells[ 0 ], 'tablesorter' ).needResort ) { resortCells; }

cell = rowspanCells.shift; cellData = $.data( cell, 'tablesorter' ); rowSpan = cell.rowSpan; colSpan = cell.colSpan; spanningRealCellIndex = cellData.realCellIndex; cell.rowSpan = 1; $nextRows = $( cell ).parent.nextAll; for ( i = 0; i < rowSpan - 1; i++ ) { $tds = $( $nextRows[ i ].cells ).filter( filterfunc ); $clone = $( cell ).clone; $clone.data( 'tablesorter', {               realCellIndex: spanningRealCellIndex,                realRowIndex: cellData.realRowIndex + i,                needResort: true            }); if ( $tds.length ) { $tds.each( fixTdCellIndex ); $tds.first.before( $clone ); } else { $nextRows.eq( i ).append( $clone ); }       }    } }

// Bind events on each button $('#' + table_id + '-controls' + ' ' + 'button.filter-table-button').click(function(e){   // Remove any rowspans    explodeRowspans( $('#' + table_id) );

// Get properties of clicked element var t = e.delegateTarget;

// Check if button has previously been pushed - adjust state as required $(t).toggleClass('filter-active');

// Collect list of active buttons var active_t = $('#' + table_id + '-controls' + ' ' + '.filter-active');

// For 'only' mode, remove other active filters before proceeding if (mode == 'only') { $.map(active_t, function(k){           if (k.attributes['data-value'].value != t.attributes['data-value'].value) {                $(k).removeClass('filter-active');            }        }); active_t = $('#' + table_id + '-controls' + ' ' + '.filter-active'); }   // Write new CSS rule to replace old one switch (mode) { case 'or': $('#' + table_id + '-css').text('#' + table_id + ' .filter-row { display: none; }' + '\n' + ( active_t.length > 0 ? $.map(active_t, function(k){                   return '#' + table_id + ' .filter-row.' + k.attributes['data-value'].value;                }).join(', ') : '#' + table_id + ' .filter-row' )                + ' { display: table-row; }'            ); break; case 'and': default: $('#' + table_id + '-css').text('#' + table_id + ' .filter-row { display: none; }' + '\n' + '#' + table_id + ' .filter-row' +                $.map(active_t, function(k){ return '.' + k.attributes['data-value'].value; }).join('') + ' { display: table-row; }'           ); break; } });

// Bind event on clear $('#' + table_id + '-controls' + ' ' + 'button.filter-table-clear').click(function(e){   // Remove associated classes to show all    $('#' + table_id + '-css').text('#' + table_id + ' .filter-row { display: table-row }');

// Remove button classes from all $('#' + table_id + '-controls' + ' ' + '.filter-table-button').removeClass('filter-active'); });

});