This page looks best with JavaScript enabled

Toggleable columns

 ·  ☕ 9 min read

In April last year, I saw a google doc with screenshots of our tables on Leaguepedia cut and pasted in MSPaint or the like so that some of the columns were removed, to make the table artificially narrower. “Ahhh,” I thought, “If only we had a way of hiding some columns on the wiki so that you could just screenshot what you wanted!” And then I realized I could in fact build that. So I did.

I’m writing this blog post about eight months after having built this feature because I think it’s a pretty cool feature that a lot of wikis could adapt, but I may have forgotten a couple details. Well, one detail. I really don’t remember the specifics of how the (truly unintuitive) widget syntax works. So I’m going to casually gloss over that and leave it as an exercise to the reader. Enjoy, reader!

A screenshot of Faker’s match history, with the Tournament checkbox hidden

You can play with this example at Faker’s match history page.

(Note: All code in this post was originally written by me for Leaguepedia and is licensed under CC BY-SA 3.0.)

Overview

The high-level overview is that I’m going to make use of the nth-of-type selector to pick which cells to show-hide when a user selects or un-selects a checkbox.

All of the boxes start out selected, and all of the columns start out shown. If you then un-select the 5th checkbox, I apply a class called column-show-hide-hidden to each cell in the 5th column. If you re-select the 5th checkbox, I remove this class. The class has one rule, which is display:none!important;. This is one of the few cases when !important; is actually appropriate.

(Why not an inline style? Because maybe some cells had an inline display:none; for another reason unrelated to this toggle, for example an entire row could be display:none;, and I don’t want to mess with that. It’s cleaner to use classes.)

Shortly after I created the MVP, I realized I also wanted a button to reset the state, so I added a “Show all” button as well.

And that’s really all there is to it from a conceptual point of view; everything else is implementation details.

CSS

This is by far the easiest part.

1
2
3
.column-show-hide-hidden {
	display:none!important;
}

Yay, hooray!

You can find this css at MediaWiki:Gadget-toggles.css.

JavaScript

Checkbox selector

I have two events. The first is for the checkboxes:

The code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
	$('.column-show-hide-toggler').off('click');
	$('.column-show-hide-toggler').click(function(e) {
		var tableIndex = $(this).closest('.column-show-hide').attr('data-table-index');
		var columnIndex = $(this).attr('name');
		var selectors = [
			'table.column-show-hide-' + tableIndex + ' > * > tr > *:nth-of-type(' + columnIndex + '):not(.colspan-cell)',
			'table.column-show-hide-' + tableIndex + ' > tr > *:nth-of-type(' + columnIndex + '):not(.colspan-cell)',
		];
		var selectorsStr = selectors.join(', ');
		$(selectorsStr).toggleClass('column-show-hide-hidden');
	});

Walkthrough

1
	$('.column-show-hide-toggler').off('click');

In case an event causes ResourceLoader to refire, I first turn off all event handlers on my selector.

1
2
		var tableIndex = $(this).closest('.column-show-hide').attr('data-table-index');
		var columnIndex = $(this).attr('name');

As for the logic of the event handler itself, I have two indices:

  1. The first is a table index, since I might have more than one table on the page that’s capable of having its columns shown or hidden via these checkboxes. So I’m going to put a parent div around this entire thing with an attribute called data-table-index, and track this through a global variable on the page (using the LuaVariables extension).
  2. Then my column index is going to be tracked through the name index on each checkbox input element.
1
2
3
4
		var selectors = [
			'table.column-show-hide-' + tableIndex + ' > * > tr > *:nth-of-type(' + columnIndex + '):not(.colspan-cell)',
			'table.column-show-hide-' + tableIndex + ' > tr > *:nth-of-type(' + columnIndex + '):not(.colspan-cell)',
		];

I have two selectors, one with > * > in between table and tr and one without that, just because MediaWiki likes to do that.

I’m skipping the show-hide on anything with a class of colspan-cell; I add that class in my HtmlUtil module any time I construct a cell with a colspan. (This is one of the reasons it’s so important and useful to have utility module wrappers for literally every circumstance; you can rely on certain conventions like this.)

1
2
		var selectorsStr = selectors.join(', ');
		$(selectorsStr).toggleClass('column-show-hide-hidden');

Then I concatenate the two selectors into a list, and my event just toggles the class column-show-hide-hidden any time I click one of these checkboxes.

Show all selector

The “show all” button selector is very similar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
	$('.column-show-hide-show-all').off('click');
	$('.column-show-hide-show-all').click(function(e) {
		$(this).closest('.column-show-hide').find('.column-show-hide-toggler').prop("checked", true);
		var tableIndex = $(this).closest('.column-show-hide').attr('data-table-index');
		var selectors = [
			'table.column-show-hide-' + tableIndex + ' > * > tr > *:not(.colspan-cell)',
			'table.column-show-hide-' + tableIndex + ' > tr > *:not(.colspan-cell)',
		];
		var selectorsStr = selectors.join(', ');
		$(selectorsStr).removeClass('column-show-hide-hidden');
	});

Note that I’m always removing the class instead of toggling it (removing the display:none; shows the element), and I don’t have any :nth-of-type in my selector, so it applies to all cells.

Widget

Now let’s build the HTML for our form input. Going into this project, I didn’t know any of the widget php syntax. Eight months later, I have once again forgotten it all. Here are some useful documentation pages:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<includeonly><div class="column-show-hide" data-table-index="<!--{$table|escape:'html'}-->">
	<button class="column-show-hide-show-all">
		Show all
	</button>
<!--{foreach from=$columns key=i item=col}-->
	<label class="column-show-hide-label">
		<input class="column-show-hide-toggler" type="checkbox" checked="true" id="table-<!--{$table|escape:'html'}-->-col-<!--{$i|escape:'html'}-->" name="<!--{$i|escape:'html'}-->"><!--{$col|escape:'html'}-->
	</label>
<!--{/foreach}-->
</div></includeonly>

In the first line, I’m making our container div, with the right class and the data-table-index attribute provided by the parameter table. I’m also escaping the parameter so that you can’t pass in arbitrary code.

I then create the show-all button, which is just static HTML, a button with the right class.

After that, I need to loop over all of my columns and print the checkboxes with the proper id, class, and name. The id must be globally unique in the entire page, not just this one instance of the widget, so the table index is included as part of the id in addition to the column index.

I read the docs and did a lot of trial and error to get this to work. As I mentioned in the introduction, I don’t really remember exactly how the loop syntax works anymore.

As an aside: note that nowhere in “important” fields am I using the display name of the column ($col). The name attribute is given by the column numerical index, and it’s just the label’s display text that uses the column name. This fact is important because it’s possible in some cases for two column labels to have the same name. For example, in the tournament player statistics table, G is reused to mean both “Games” and “Gold” (with title attributes explaining the two meanings). Of course, the very existence of the title attributes (and also spaces!) also makes using words inconvenient here, as I’d have to do a bunch of escaping etc.

The takeaway is: any time you’re creating “hidden” attributes, ids, etc, go with automatically-incrementing numerical keys instead of things named by users if you can.

Lua

Finally, we need to call the widget from Lua. I wrote Module:ColumnShowHide with two public functions, printToggles and printTableClass.

The important thing that enables me to “just do this” is that I always, always, always have an array containing all of my column names. This fact may seem like not a big deal, but it’s a big deal. Why? Because I can reuse this array:

  1. to make my actual list of headings and
  2. to send it to the widget in order to print all of these nice column-toggle labels.

(Actually (2) happens before (1), but the order of intentionality is (1) then (2).)

This way, the two sets of things will always match.

(Incidentally, it’s also mandatory to use the mw.html library (or a custom alternative that works with objects) instead of just working with raw HTML strings if you want to do something like this in a remotely-sane manner.)

Calling Module:ColumnShowHide from another module (in this example, MatchHistoryPlayer, the module I showed in the introduction) looks like this:

1
2
3
4
	local output = h.initializeOutput()
	ColumnShowHide.printToggles(output, PRELOAD.headings)
	local tbl = h.initializeTable(output, args)
	ColumnShowHide.printTableClass(tbl)

And here’s the code for Module:ColumnShowHide:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
local util_math = require("Module:MathUtil")
local util_vars = require("Module:VarsUtil")
local i18n = require("Module:I18nUtil")

local h = {}
local p = {}
function p.printToggles(output, COLUMNS)
	i18n.init('ColumnShowHide')
	if not output then output = mw.html.create() end
	output:wikitext(i18n.print('introText'), mw.getCurrentFrame():callParserFunction{
		name = '#widget',
		args = h.getWidgetArgs(COLUMNS)
	})
	return output
end

function p.printTableClass(tbl)
	if not tbl then tbl = mw.html.create('table') end
	tbl:addClass('column-show-hide-' .. h.getTableIndex())
	return tbl
end

function h.getWidgetArgs(COLUMNS)
	local ret = {
		'ColumnShowHide',
		table = h.setTableIndex()
	}
	for i, col in ipairs(COLUMNS) do
		ret['columns.' .. util_math.padleft(i, 2)] = i18n.print(col) or col
	end
	return ret
end

function h.getTableIndex()
	return util_vars.getGlobalIndex('COLUMN_SHOW_HIDE_WIDGET_INDEX')
end

function h.setTableIndex()
	return util_vars.setGlobalIndex('COLUMN_SHOW_HIDE_WIDGET_INDEX')
end

return p

See how I use COLUMNS here?

And then in Module:MatchHistoryPlayer, I used the following function to print my actual column labels:

1
2
3
4
5
6
7
8
function h.printHeadings(tbl)
	local tr = tbl:tag('tr')
		:addClass(h.getClassName('headings'))
	for _, col in ipairs(PRELOAD.headings) do
		tbl:tag('th')
			:wikitext(i18n.print(col))
	end
end

(I have a function in Module:HtmlUtil that I’d normally call directly to print headings, but with a settings file my headings table isn’t formatted exactly right for that function to get the right classes.)

Anyway, most of Module:ColumnShowHide is constructing the arguments in the exact format that the widget wants.

The i18n file has one line:

1
2
3
4
5
return {
	['en'] = {
		introText = 'Toggle columns: '
	},
}

Conclusion

Thanks to some guarantees about the HTML structure of my pages that I can rely on from my consistent use of utility wrappers in Lua, I’m able to have an extremely simple interface to create column show-hide buttons at the top of any data table I create; just two functions have to be called from the module I’m writing, and everything else happens magically behind the scenes.

The components that go into this are:

  • A css class - never manipulate inline styles!
  • Two event handlers that work using nth-of-type selectors, and check to make sure they’re within the correct parent element on the page via a global index in case the page holds two or more applicable tables.
  • A widget, so that form elements are available.
  • And a Lua module, to create the interface from code to the widget. This makes use of a singleton state via the VariablesLua extension to manage that global table index and handles indexing and formatting the column names within each table for the widgets extension.
Share on

river
WRITTEN BY
River
River (RheingoldRiver) is a MediaWiki developer and the manager of Leaguepedia. She likes cats.


What's on this Page