This page looks best with JavaScript enabled

Gadget - Copy title

 ·  ☕ 8 min read

Motivation

I’m really lazy. Also, I used to get pretty bad wrist pain in both of my wrists, making both typing and clicking pretty painful, so the act of double-clicking or click-and-dragging page titles in MediaWiki was super frustrating to me. I really wanted a way to click a single button and get something in my clipboard.

So I wrote a gadget to give me buttons that do exactly this: when clicked, they’ll copy the page name to your clipboard. Because sometimes you want the full name of a page, and sometimes you want only the title (without namespace), there are two of them; the top one gives you the title and the bottom one gives you the full page name. My mnemonic for remembering which is which is that the “smaller” one is sitting on top of the “bigger” one, but if you wanted to you could pretty easily replace the icon in the buttons with some text, e.g. Copy title & Copy page name or Copy title & Copy prefixed title if you don’t have the $wg variable names memorized.

It looks like this:

As this is a tool for myself, it is not accessible - you can’t tab to these buttons, and you can’t click them via keyboard shortcuts. Please don’t emulate this design for production-ready tools that anyone should be able to use.

Code

JS

 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
$(function() {
	var el = document.createElement('div')
	$(el).attr('id', 'title-copy-outer').html('<div id="title-copy-content" title="Copy Only Title (No Namespace)"></div><div id="title-copy-all" title="Copy Full Title (Incl Namespace)"></div>');
	$('#firstHeading').wrapInner('<div id="first-heading-text"></div>');
	$(el).insertAfter(document.getElementById('first-heading-text'));
	
	function doCopy(e, text) {
		navigator.clipboard.writeText(text);
		$(e).css('color','green');
		setTimeout(function() {
			$(e).css('color','');
		}, 2000);
	}	
	
	$('#title-copy-content').click(function() {
		doCopy('#title-copy-content', mw.config.get('wgTitle')
			.replace(/(WhatLinksHere|MovePage)\/(.+?:)?/,'')
		);
	});
		$('#title-copy-all').click(function() {
		doCopy('#title-copy-all', mw.config.get('wgPageName')
			.replace(/Special:(?:WhatLinksHere|MovePage)\//,'')
			.replace(/_/g, " ")
		);
	});

});

CSS

 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
@font-face {
font-family: "FontAwesome";
font-weight: normal;
font-style: normal;
	src: url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.eot?v=4.7.0");
	src: url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),
		url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),
		url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),
		url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),
		url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");
}

#title-copy-outer {
  margin-left:0.25em;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

#title-copy-content,
#title-copy-all {
  cursor:pointer;
}

#title-copy-content::after,
#title-copy-all::after {
  content:'\f0c5';
  font-family: fontAwesome;
  font-size:50%;
  display:block;
  line-height:1; /* in case it's changed in the local wiki css */
}

#firstHeading, #first-heading-text {
  display:flex;
  align-items:self-start;
}

Explanation

JS

First, we’re going to create an element with the following DOM structure:

1
2
3
4
<div id="title-copy-outer">
    <div id="title-copy-content" title="Copy Only Title (No Namespace)"></div>
    <div id="title-copy-all" title="Copy Full Title (Incl Namespace)"></div>
</div>

We’ll do that using jQuery:

1
2
3
var el = document.createElement('div')
$(el).attr('id', 'title-copy-outer')
  .html('<div id="title-copy-content" title="Copy Only Title (No Namespace)"></div><div id="title-copy-all" title="Copy Full Title (Incl Namespace)"></div>');

And then we’ll insert this to our document inside of the #firstHeading div, first wrapping the existing content in its own small container, so that if we use a display:flex;, the elements that are already there stay grouped together.

1
2
$('#firstHeading').wrapInner('<div id="first-heading-text"></div>');
$(el).insertAfter(document.getElementById('first-heading-text'));

Next, we’ll do the copy:

1
2
3
4
5
6
function doCopy(e, text) {
      navigator.clipboard.writeText(text);
      
      // Then give some feedback to the user that the action went through, by changing
      // the color of the button to green
}	

Here’s the feedback to the user:

1
2
3
4
$(e).css('color','green'); // change color to green
setTimeout(function() { // then wait to do the next part
  $(e).css('color',''); // remove the green color
}, 2000); // 2000 milliseconds (2 seconds) is how long to wait

If you click the first one (#title-copy-content), then we’ll copy the $wgTitle variable (available via mw.config.get('wgTitle')); and if you click the second one (#title-copy-all) then we’ll copy $wgPageName instead. Title is defined as the title of the page (without namespace) and PageName is defined as the title of the page (with namespace), so this is what we want.

(Unlike $wgTitle, $wgPageName comes with underscores instead of spaces, and probably that’s not what you actually want to copy, so for the full-page-name case we also replace _ with .)

But there are a couple exceptions; namely, Special:WhatLinksHere and Special:MovePage. These are both common things to check or do with pages, and we might want to a copy a title from one of these two locations. But they aren’t technically actions (i.e. the URL isn’t in the form ?action=move); the wgTitle of Special:WhatLinksHere/User:RheingoldRiver isn’t RheingoldRiver, but rather WhatLinksHere/User:RheingoldRiver. You can confirm this by typing mw.config.get('wgTitle') at any Special:WhatLinksHere page.

To fix this issue, we’ll fudge things a bit and compute the intended values.1 For copying the wgTitle of the page we actually want, we have this regex:

1
/(WhatLinksHere|MovePage)\/(.+?:)?/

Here is what each part of that regex means:

Text Meaning
(WhatLinksHere|MovePage) Either WhatLinksHere or MovePage (title must start with one of these). Because we’re replacing with the empty string, we don’t have to worry about capture groups, so we’re safe to write (content here) and not (?:content here).
\/ A literal / character. This has to be escaped, because in JS / is the quote character for regular expressions, like /regex here/.
()? An optional capture group; that is, if it’s found select it, but if not it’s ok, don’t cancel the entire match over it.
.+? 1 or more characters, but instead of selecting as many as possible, select as few as possible. For example, while ca+ matching caaaaaaaaaat returns caaaaaaaaaa, ca+? returns ca. This is called a “lazy” selector (as opposed to being “greedy”).
: The literal : character. Because we are looking for .+?: we are going to remove only up to the first : character, so if you have a page called something like User:RheingoldRiver/Template:Kittens you’ll get RheingoldRiver/Template:Kittens (which is the actual page name) instead of Kittens.

The wgPageName case is similar, but we also remove Special: at the start, since the wgPageName of Special:WhatLinksHere/Template:SomeTemplate includes Special: at the start, and we want just Template:SomeTemplate in this case. Also, $wgPageName uses underscores instead of spaces, so we’ll globally replace _ with : .replace(/_/g, " ").

CSS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@font-face {
font-family: "FontAwesome";
font-weight: normal;
font-style: normal;
	src: url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.eot?v=4.7.0");
	src: url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),
		url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),
		url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),
		url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),
		url("https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");
}

First, we import the FontAwesome font. FontAwesome provides a bunch of useful characters that are hosted for free so you can use them anywhere on the web. Think of it as a modern-day Wingdings for the internet.

For best compatibility, there are a bunch of imports in this list, but browsers will only load one of them.

We’re going to add a FontAwesome copy icon like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#title-copy-content::after,
#title-copy-all::after {
  content:'\f0c5'; /* From the upper-right corner of the FontAwesome page for this icon */
  font-family: fontAwesome;
  
  /* We want two of these vertically, so this is a rare case where
  percent-based sizing is appropriate */
  font-size:50%;
  display:block; /* you usually need this on any pseudoelement you want to show up */
}

In the actual elements of #title-copy-content and #title-copy-all all we have to do is apply cursor:pointer; to signify to the user they can click these things.

For container holding #title-copy-content and #title-copy-all, we need to apply a bit of spacing to the left and make super duper sure no one will accidentally copy these when using click-and-drag to copy text. The unprefixed support for this is actually pretty low so we have a bunch of variants to make sure it works in Webkit etc. I’m pretty sure I copied this list from StackOverflow.

1
2
3
4
5
6
7
8
9
#title-copy-outer {
  margin-left:0.25em;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

Finally we’ll make the entire container flex and align stuff at the start; this is basically equivalent to making everything display:inline-block;, but it’s a bit more extensible if we add more things in this toolbar later:

1
2
3
4
#firstHeading {
  display:flex;
  align-items:self-start;
}

Conclusion

The most complicated part of this gadget is probably the regular expression to fix the text for Special:WhatLinksHere and Special:MovePage special cases. The other big idea here is using pseudoelements to display icons via CSS, which is a very powerful pattern especially when working with FontAwesome. Happy coding!


  1. In fact as written the code doesn’t work exactly correctly in all cases; if you have a page called NotANamespace:Some Title, where NotANamespace isn’t a namespace, you’ll lose that prefix even when trying to copy the full page name, and if you have a namespace with a colon in it, the wgTitle case will fail. That said, both of these cases are really terrible and you should make an effort to avoid either one (especially don’t put a : in any namespace name or your wiki title (which is the project namespace name)!). ↩︎

Share on

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

What's on this Page