Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions javascript/Main.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const jsModules = [
'Selectall',
'Slider',
'SwitchButtons',
'Table2',
'Tabs'
];

Expand Down
134 changes: 134 additions & 0 deletions javascript/commons/Table2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*******************************************************************************
* Description: Handles dynamic striping for Table2 widgets,
* applying odd/even classes with rowspan grouping support.
******************************************************************************/

const TABLE2_CONFIG = {
SELECTORS: {
TABLE: '.table2 .table2__table',
BODY_ROW: 'tbody tr.table2__row--body',
ALL_ROWS: 'tbody tr'
},
CLASSES: {
EVEN: 'table2__row--even',
HEAD: 'table2__row--head'
}
};

class Table2Striper {
constructor( table ) {
this.table = table;
this.isSortable = table.classList.contains( 'sortable' );
this.shouldStripe = table.getAttribute( 'data-striped' ) !== 'false';
this.groupCounter = 0;
}

init() {
if ( this.shouldStripe ) {
this.restripe();

if ( this.isSortable ) {
this.setupSortListener();
}
}
this.setupHoverListeners();
}

setupSortListener() {
mw.loader.using( 'jquery.tablesorter' ).then( () => {
$( this.table ).on( 'sortEnd.tablesorter', () => this.restripe() );
} );
}

restripe() {
const rows = this.table.querySelectorAll( TABLE2_CONFIG.SELECTORS.ALL_ROWS );
let isEven = true;
let groupRemaining = 0;
this.groupCounter = 0;

rows.forEach( ( row ) => {
const isBodyRow = row.classList.contains( 'table2__row--body' );
const isSubheader = row.classList.contains( TABLE2_CONFIG.CLASSES.HEAD );

if ( isSubheader ) {
isEven = true;
groupRemaining = 0;
row.removeAttribute( 'data-group-id' );
return;
}

if ( !isBodyRow || row.style.display === 'none' ) {
return;
}

if ( groupRemaining === 0 ) {
isEven = !isEven;
this.groupCounter++;
}

const rowspanCount = parseInt( row.dataset.rowspanCount, 10 ) || 1;
groupRemaining = Math.max( groupRemaining, rowspanCount );

row.classList.toggle( TABLE2_CONFIG.CLASSES.EVEN, isEven );
row.setAttribute( 'data-group-id', this.groupCounter );

groupRemaining--;
} );
}

setupHoverListeners() {
const bodyRows = this.table.querySelectorAll( TABLE2_CONFIG.SELECTORS.BODY_ROW );

bodyRows.forEach( ( row ) => {
row.addEventListener( 'mouseenter', ( e ) => this.onRowHoverEnter( e ) );
row.addEventListener( 'mouseleave', ( e ) => this.onRowHoverLeave( e ) );
} );
}

onRowHoverEnter( event ) {
const row = event.target.closest( TABLE2_CONFIG.SELECTORS.BODY_ROW );
if ( !row ) {
return;
}

const groupId = row.getAttribute( 'data-group-id' );
if ( !groupId ) {
return;
}

const groupRows = this.table.querySelectorAll(
`${ TABLE2_CONFIG.SELECTORS.BODY_ROW }[data-group-id="${ groupId }"]`
);
groupRows.forEach( ( r ) => r.classList.add( 'table2__row--group-hover' ) );
}

onRowHoverLeave( event ) {
const row = event.target.closest( TABLE2_CONFIG.SELECTORS.BODY_ROW );
if ( !row ) {
return;
}

const groupId = row.getAttribute( 'data-group-id' );
if ( !groupId ) {
return;
}

const groupRows = this.table.querySelectorAll(
`${ TABLE2_CONFIG.SELECTORS.BODY_ROW }[data-group-id="${ groupId }"]`
);
groupRows.forEach( ( r ) => r.classList.remove( 'table2__row--group-hover' ) );
}
}

class Table2Module {
init() {
const tables = document.querySelectorAll( TABLE2_CONFIG.SELECTORS.TABLE );

tables.forEach( ( table ) => {
new Table2Striper( table ).init();
} );
}
}

liquipedia.table2 = new Table2Module();
liquipedia.core.modules.push( 'table2' );
98 changes: 98 additions & 0 deletions javascript/tests/Table2.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* @jest-environment jsdom
*/
/* global jest */

const { test, expect, beforeAll, describe } = require( '@jest/globals' );

describe( 'Table2 module', () => {
beforeAll( () => {
globalThis.liquipedia = {
core: {
modules: []
}
};

globalThis.mw = {
loader: {
using: jest.fn( () => Promise.resolve() )
}
};

require( '../commons/Table2.js' );
} );

test( 'should register itself as a module', () => {
expect( globalThis.liquipedia.core.modules ).toContain( 'table2' );
} );

test( 'should initialize table2 instance', () => {
expect( globalThis.liquipedia.table2 ).toBeTruthy();
} );

test( 'should have init method', () => {
expect( typeof globalThis.liquipedia.table2.init ).toBe( 'function' );
} );

test( 'should assign data-group-id to body rows during striping', () => {
document.body.innerHTML = `
<div class="table2">
<table class="table2__table">
<tbody>
<tr class="table2__row--body"></tr>
<tr class="table2__row--body"></tr>
<tr class="table2__row--body"></tr>
</tbody>
</table>
</div>
`;

globalThis.liquipedia.table2.init();

const rows = document.querySelectorAll( '.table2 .table2__table tbody tr.table2__row--body' );
expect( rows.length ).toBe( 3 );

rows.forEach( ( row ) => {
expect( row.getAttribute( 'data-group-id' ) ).toBeTruthy();
} );
} );

test( 'should apply even class to alternating rows', () => {
document.body.innerHTML = `
<div class="table2">
<table class="table2__table">
<tbody>
<tr class="table2__row--body"></tr>
<tr class="table2__row--body"></tr>
<tr class="table2__row--body"></tr>
<tr class="table2__row--body"></tr>
</tbody>
</table>
</div>
`;

globalThis.liquipedia.table2.init();

const rows = document.querySelectorAll( '.table2 .table2__table tbody tr.table2__row--body' );

expect( rows[ 0 ].classList.contains( 'table2__row--even' ) ).toBe( false );
expect( rows[ 1 ].classList.contains( 'table2__row--even' ) ).toBe( true );
expect( rows[ 2 ].classList.contains( 'table2__row--even' ) ).toBe( false );
expect( rows[ 3 ].classList.contains( 'table2__row--even' ) ).toBe( true );
} );

test( 'should ignore standalone tables without .table2 wrapper', () => {
document.body.innerHTML = `
<table class="table2__table">
<tbody>
<tr><td>Standalone table row</td></tr>
</tbody>
</table>
`;

globalThis.liquipedia.table2.init();

const row = document.querySelector( '.table2__table tbody tr' );
expect( row.getAttribute( 'data-group-id' ) ).toBeNull();
} );
} );
1 change: 0 additions & 1 deletion lua/wikis/commons/Widget/Contexts/Table2.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ local Class = Lua.import('Module:Class')
local Context = Lua.import('Module:Widget/Context')

return {
BodyStripe = Class.new(Context),
ColumnContext = Class.new(Context),
HeaderRowKind = Class.new(Context),
Section = Class.new(Context),
Expand Down
36 changes: 24 additions & 12 deletions lua/wikis/commons/Widget/Table2/Row.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,25 @@ local HtmlWidgets = Lua.import('Module:Widget/Html/All')
---@field props Table2RowProps
local Table2Row = Class.new(Widget)

---@param rowChildren (Widget|Html|string|number|nil)[]
---@return integer|nil
local function getMaxRowspan(rowChildren)
local maxRowspan = Array.reduce(rowChildren, function(max, child)
if Class.instanceOf(child, Table2Cell) or Class.instanceOf(child, Table2CellHeader) then
local cellChild = child --[[@as Table2Cell|Table2CellHeader]]
local rowspan = MathUtil.toInteger(cellChild.props.rowspan) or 1
return math.max(max, math.max(rowspan, 1))
end
return max
end, 1)
return maxRowspan > 1 and maxRowspan or nil
end

---@return Widget
function Table2Row:render()
local props = self.props
local section = props.section or self:useContext(Table2Contexts.Section)
local headerRowKind = self:useContext(Table2Contexts.HeaderRowKind)
local bodyStripe = self:useContext(Table2Contexts.BodyStripe)

local sectionClass = 'table2__row--body'
if section == 'head' or section == 'subhead' then
Expand All @@ -53,15 +66,6 @@ function Table2Row:render()
end
end

local stripeClass
if section == 'body' then
if bodyStripe == 'odd' then
stripeClass = 'table2__row--odd'
elseif bodyStripe == 'even' then
stripeClass = 'table2__row--even'
end
end

local highlightClass
if section == 'body' and Logic.readBool(props.highlighted) then
highlightClass = 'table2__row--highlighted'
Expand Down Expand Up @@ -111,10 +115,18 @@ function Table2Row:render()
end)
end

local attributes = props.attributes or {}
if section == 'body' then
local maxRowspan = getMaxRowspan(children)
if maxRowspan then
attributes['data-rowspan-count'] = maxRowspan
end
end

return HtmlWidgets.Tr{
classes = WidgetUtil.collect(sectionClass, kindClass, stripeClass, highlightClass, props.classes),
classes = WidgetUtil.collect(sectionClass, kindClass, highlightClass, props.classes),
css = props.css,
attributes = props.attributes,
attributes = attributes,
children = trChildren,
}
end
Expand Down
10 changes: 4 additions & 6 deletions lua/wikis/commons/Widget/Table2/Table.lua
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,13 @@ function Table2:render()
}}
end

if Logic.readBool(props.striped) then
tableChildren = {Table2Contexts.BodyStripe{
value = true,
children = tableChildren,
}}
local tableAttributes = props.tableAttributes or {}
if not Logic.readBool(props.striped) then
tableAttributes['data-striped'] = 'false'
end

local tableNode = HtmlWidgets.Table{
attributes = props.tableAttributes,
attributes = tableAttributes,
classes = tableClasses,
children = tableChildren,
}
Expand Down
Loading
Loading