This page looks best with JavaScript enabled

Gadget - Copy category members

 ·  ☕ 6 min read

A screenshot of the !Copy members button

This time, we’ll add a button to the #p-cactions dropdown (how to do that was initially introduced in Gadget - Search results new page hotkey) that gives you a button to click to copy all of the members of a category, one per line.

A screenshot of the inserted copyable category members textarea

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


Sometimes we want to copy all of the members of a category. This could be to:

  • Ctrl+F through all of the titles in Notepad++
  • Create a pairsfile for PWB’s movepages script - copy all the category members, then run a regex to generate the destination (something like $1\r\n$1 - new as the replacement term with (.*) as the search to move from pagename to pagename - new)
  • Provide a list of pages to someone who may be less comfortable with built-in features of tooling, but still wants to run tooling operations on a category (see below)

Things we would (probably) not need this for:

  • Working with AutoWikiBrowser - it has a category select as a generator option, and pretty robust filtering options of its own
  • Working with PWB if it’s not to move pages - -cat: or -categoryname: is a generator
  • Writing Python scripts - we’d just use site.categories from mwclient to get the listing (site.client.categories if working with mwcleric)

Can’t we just highlight and Ctrl+C?

There’s a couple issues with copying the page list as-is:

  • You get the single-letter section titles included as well
  • The category could span more than one page, so not all members are included (though this gadget doesn’t actually continue, so it’s limited at max members which for my purposes is 5000 since I have apihighlimits on every wiki I work with)
  • There’s leading whitespace at the beginning of each line

So it’s pretty annoying to sanitize the input we’d get if we just copied what’s already on the page. (Exercise to the reader - generate a single regex to sanitize the input we get! You’ll have to use at least one pipe.)


$(function() {
	wgNamespace = mw.config.get('wgCanonicalNamespace');
	if (wgNamespace != 'Category') return;
	$(mw.util.addPortletLink('p-cactions', 'javascript:;', '!Copy Members', 'ca-copy-cat-members', 'Copy Category Members', null, '#ca-move-to-user')).click(function() {
		a = new mw.Api();
			action : 'query',
			list : 'categorymembers',
			cmtitle : mw.config.get('wgPageName'),
			cmlimit : 'max'
		}).then(function(data) {
			tbl = [];
			for (page in data.query.categorymembers) {
			var str = tbl.join('\n');
			displayOutputText(str, true);

It uses this function from Gadget - utils.js:

window.displayOutputText = function(str, highlight) {
	var el = document.createElement('textarea');
	el.value = str;
	$(el).css('height', '200px')
		.attr('readonly', '')
		.attr('id', 'gadget-output-display');
	if (highlight) {;


First, immediately return if we’re not on a category page. Then add a portlet link into p-cactions with our action; I’ve explained this before.

API query

Next, we do an API call: a simple query (which is a get request as opposed to a postWithToken) of the list of categorymembers. We can check the categorymembers documentation to see what parameters are supported, though I almost always just use the API sandbox to check parameters.

We want to specify two parameters:

  • cmtitle - this is the title of the category we’re querying, and in this case it’s the name of the current page we’re on
  • cmlimit - set it to max; by default, if we’re an admin, it’ll be 5000, and this should be plenty


Note that this code does NOT support copying the entire category’s members for “large” categories, and if you don’t have the apihighlimits right, you’ll be gated at only 500 results (by default). If we wanted to change this, we’d have to restructure the code slightly, adding another function called something like doQuery that accepts a cmcontinue parameter as well as the current value of our list of known pages. Our then would then append the new data, and if we have a query-continue as part of our response from the server (i.e. if we’re not done getting data), would re-call doQuery with the next value of cmcontinue set to the query-continue value.

If we were to take this approach, we’d probably want to set our own limit (perhaps 5000) so that (a) we’re never inserting a list of say 50,000 elements to the DOM if a user calls !Copy members unwittingly on a huge category page and also (b) we’re not making the user wait for dozens of sequential round trips to the server and back before the result is ready.


After we’ve gotten our result, we’ll iterate over the result and push the title of each page into an array, concatenate the result, and then display the output text.

A screenshot of the HTML of a category page, showing the #contentSub element highlighted

The displayOutputText function makes a textarea, sets it to readonly, and then places it right after #contentSub. It highlights and selects the text inside, but doesn’t execute any copy operation, in case you have sensitive information on your clipboard already.

Why delegate to a helper function instead of just inserting the element immediately? This code is pretty simple.

  • Laziness/ease - there’s a few different gadgets that create copyable output like this, and I’d rather not copy-paste the same code multiple times
  • Single point of change - if the DOM ever changes so that #contentSub is no longer the place I’m going to insert the text (hey, that’s happening!) or if I ever want to change other CSS properties about it, there’s a single place to do so

TL;DR: I actually do use it enough that DRY applies.


Like many of my tools, this gadget is a pretty quick and somewhat lazy but still very useful piece of code - effectively I do only 10% of a proper implementation (which would incorporate continuing) but get 90% of the benefit (continuing is almost never needed). Because I make it available only by opt-in, instead of enabling it by default, its roughness is less of a problem, and it’s always open to extension and completion if someone’s needs are ever not being met.

You should always feel free to write incomplete tools and use them! A hacked-together script that does 90% of what you could imagine it capable of doing can be among your most valuable assets! Just don’t mistake incomplete for untested or incorrect; your incomplete tools should still work.

Share on

River is a developer most at home in MediaWiki and known for building Leaguepedia. She likes cats.

What's on this Page