O'Reilly Hacks
oreilly.comO'Reilly NetworkSafari BookshelfConferences Sign In/My Account | View Cart   
Book List Learning Lab PDFs O'Reilly Gear Newsletters Press Room Jobs  


 
Buy the book!
Greasemonkey Hacks
By Mark Pilgrim
November 2005
More Info

HACK
#75
Add a Text-Sizing Toolbar to Web Forms
Insert buttons before <textarea> elements to make the text larger or smaller
The Code
[Discuss (0) | Link to this hack]

The Code

This user script runs on all pages. The code looks complicated, and it is complicated, but not for the reason you think. It looks complicated because of the large multiline gibberish-looking strings in the middle of it. Those are data: URIs, which look like hell but are easy to generate.(See "Embed Graphics in a User Script" for more on data: URIs.)

The toolbar is displayed visually as a row of buttons, but each button is really just an image of something that looks pushable, wrapped in a link that executes one of our JavaScript functions. Since we'll be creating more than one button (this script has only two, but you could easily extend it with more functionality), I created a function to encapsulate all the button-making logic:

	function createButton(target, func, title, width, height, src)

The createButton function takes six arguments:

target

An element object; the <textarea> element that this button will control.

func

A function object; the JavaScript function to be called when the user clicks the button with the mouse or activates it with the keyboard.

title

A string; the text of the tool tip when the user moves her cursor over the button.

width

An integer; the width of the button. This should be the width of the graphic given in the src argument.

height

An integer; the height of the button. This should be the height of the graphic given in the src argument.

src

A string; the URL, path, or data: URI of the button graphic.

Creating the image is straightforward, but creating the link that contains the image is where the real complexity lies:

	button = document.createElement('a');
	button._target = target;
	button.title = title;
	button.href = '#';
	button.onclick = func;
	button.appendChild(img);

There are two things I want to point out here. First, I need to assign a bogus href attribute to the link; otherwise, Firefox would treat it as a named anchor and wouldn't add it to the tab index (i.e., you wouldn't be able to tab to it, making it inaccessible with the keyboard). Second, I'm setting the _target attribute to store a reference to the target <textarea>. This is perfectly legal in JavaScript; you can create new attributes on an object just by assigning them a value. I'll access the custom _target attribute later, in the onclick event handler.

If you read Mozilla's documentation on the Event object, you'll see that there are several target-related properties, including one simply called target. You might be tempted to use event.target to get a reference to the clicked link, but it behaves inconsistently. When the user tabs to the button and presses Enter, event.target is the link, but when the user clicks the button with the mouse, event.target is the image inside the link! In any case, event.currentTarget returns the link in all cases, so I use that.

TIP

See http://www.xulplanet.com/references/objref/event.html for documentation on the Event object.

Now the real fun begins. (And you thought you were having fun already!) I need to get the current dimensions and font size of the <textarea> so that I can make them bigger. Simply retrieving the appropriate attributes from textarea.style (textarea.style.width, textarea.style.height, and textarea.style.fontSize) will not work, because those only get set if the page actually defined them in a style attribute on the <textarea> itself. That's not what I want; I want the final style, after all stylesheets have been applied. For that, I need getComputedStyle:

	s = getComputedStyle(textarea, "");
	textarea.style.width = (parseFloat(s.width) * 1.5) + "px";
	textarea.style.height = (parseFloat(s.height) * 1.5) + "px";
	textarea.style.fontSize = (parseFloat(s.fontSize) + 7.0) + 'px';

Finally, do you remember that bogus href value I added to my button link to make sure it was keyboard-accessible? Well, it's now become an annoyance, because after Firefox finishes executing the onclick handler, it's going to try to follow that link. Since it points to a nonexistent anchor, Firefox is going to jump to the top of the page, regardless of where the button is. This is annoying, and to stop it, I need to call event.preventDefault( ) before finishing my onclick handler:

	event.preventDefault();

All this was just for the sake of keyboard accessibility. What can I say? Some people build model airplanes. I build accessible web pages.

Save the following user script as zoomtextarea.user.js:

	// ==UserScript==
	// @name Zoom Textarea
	// @namespace http://diveintomark.org/projects/greasemonkey/
	// @description add controls to zoom textareas
	// @include *
	// ==/UserScript==

	function addEvent(oTarget, sEventName, fCallback, bCapture) {
		var bReturn = false;
		if (oTarget.addEventListener) {
			oTarget.addEventListener(sEventName, fCallback, bCapture);
			bReturn = true;
		} else if (oTarget.attachEvent) {
			bReturn = oTarget.attachEvent('on' + sEventName, fCallback);
		}
		return bReturn;
	}

	function createButton(elmTarget, funcCallback, sTitle, iWidth, iHeight, urlSrc) {
		var elmImage = document.createElement('img');
		elmImage.width = iWidth;
		elmImage.height = iHeight;
		elmImage.style.borderTop = elmImage.style.borderLeft = "1px solid #ccc";
		elmImage.style.borderRight = elmImage.style.borderBottom = "1px solid #888";
		elmImage.style.marginRight = "2px";
		elmImage.src = urlSrc;

		var elmLink = document.createElement('a');
		elmLink.title = sTitle;
		elmLink.href = '#';
		addEvent(elmLink, 'click', funcCallback, true);
		elmLink.appendChild(elmImage);
		return elmLink;
	}

	var arTextareas = document.getElementsByTagName('textarea');
	for (var i = arTextareas.length - 1; i >= 0; i--) {
		var elmTextarea = arTextareas[i];

		function textarea_zoom_in(event) {
			var style = getComputedStyle(elmTextarea, "");
			elmTextarea.style.width = (parseFloat(style.width) * 1.5) + "px";
			elmTextarea.style.height = (parseFloat(style.height) * 1.5) + "px";
			elmTextarea.style.fontSize = (parseFloat(style.fontSize) + 7.0) +
	'px';
			event.preventDefault( );
		}

		function textarea_zoom_out(event) {
			var style = getComputedStyle(elmTextarea, "");
			elmTextarea.style.width = (parseFloat(style.width) * 2.0 / 3.0) +
	"px";
			elmTextarea.style.height = (parseFloat(style.height) * 2.0 / 3.0) +
	"px";
			elmTextarea.style.fontSize = (parseFloat(style.fontSize) - 7.0) +
	"px";
			event.preventDefault( );
		}

		elmTextarea.parentNode.insertBefore(
			createButton(
				elmTextarea,
				textarea_zoom_in,
				'Increase text size',
				20,
				20,
				'data:image/gif;base64,'+
	'R0lGODlhFAAUAOYAANPS1tva3uTj52NjY2JiY7KxtPf3%2BLOys6WkpmJiYvDw8fX19vb'+
	'296Wlpre3uEZFR%2B%2Fv8aqpq9va3a6tr6Kho%2Bjo6bKytZqZml5eYMLBxNra21JSU3'+
	'Jxc3RzdXl4emJhZOvq7KamppGQkr29vba2uGBgYdLR1dLS0lBPUVRTVYB%2Fgvj4%2BYK'+
	'Bg6SjptrZ3cPDxb69wG1tbsXFxsrJy29vccDAwfT09VJRU6uqrFlZW6moqo2Mj4yLjLKy'+
	's%2Fj4%2BK%2Busu7t783Nz3l4e19fX7u6vaalqNPS1MjHylZVV318ftfW2UhHSG9uccv'+
	'KzfHw8qqqrNPS1eXk5tvb3K%2BvsHNydeLi40pKS2JhY2hnalpZWlVVVtDQ0URDRJmZm5'+
	'mYm11dXp2cnm9vcFxcXaOjo0pJSsC%2FwuXk6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
	'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC'+
	'H5BAAAAAAALAAAAAAUABQAAAeagGaCg4SFhoeIiYqKTSQUFwgwi4JlB0pOCkEiRQKKRxM'+
	'gKwMGDFEqBYpPRj4GAwwLCkQsijwQBAQJCUNSW1mKSUALNiVVJzIvSIo7GRUaGzUOPTpC'+

	'igUeMyNTIWMHGC2KAl5hCBENYDlcWC7gOB1LDzRdWlZMAZOEJl83VPb3ggAfUnDo5w%2F'+
	'AFRQxJPj7J4aMhYWCoPyASFFRIAA7'),
			elmTextarea);
		elmTextarea.parentNode.insertBefore(
			createButton(
				elmTextarea,
				textarea_zoom_out,
				'Decrease text size',
				20,
				20,
				'data:image/gif;base64,'+
	'R0lGODlhFAAUAOYAANPS1uTj59va3vDw8bKxtGJiYrOys6Wkpvj4%2BPb29%2FX19mJiY'+
	'%2Ff3%2BKqqrLe3uLKytURDRFpZWqmoqllZW9va3aOjo6Kho4KBg729vWJhZK%2BuskZF'+
	'R4B%2FgsLBxHNydY2Mj%2Ff396amptLS0l9fX9fW2dDQ0W1tbpmZm8DAwfT09fHw8n18f'+
	'uLi49LR1V5eYOjo6VBPUa6tr769wEhHSNra20pJStPS1KuqrNPS1ZmYm%2B7t77Kys8rJ'+
	'y%2Fj4%2BaSjpm9uca%2BvsMjHyqalqHRzdVJRU8PDxVRTVcvKzc3Nz0pKS9rZ3evq7MC'+
	'%2FwsXFxp2cnnl4e1VVVu%2Fv8ba2uM7Oz29vcbu6vZqZmnJxc9vb3PHx8uXk5mhnamJh'+
	'Y1xcXZGQklZVV29vcHl4eoyLjKqpq6Wlpl1dXuXk6AAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
	'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
	'AAACH5BAAAAAAALAAAAAAUABQAAAeZgGaCg4SFhoeIiYqKR1IWVgcyi4JMBiQqA0heQgG'+
	'KQTFLPQgMCVocBIoNNqMgCQoDVReKYlELCwUFI1glEYorOgopWSwiTUVfih8dLzRTKA47'+
	'Ek%2BKBGE8GEAhFQYuPooBOWAHY2ROExBbSt83QzMbVCdQST8Ck4QtZUQe9faCABlGrvD'+
	'rB4ALDBMU%2BvnrUuOBQkE4NDycqCgQADs%3D'),
			elmTextarea);
		elmTextarea.parentNode.insertBefore(
			document.createElement('br'),
			elmTextarea);
	}


O'Reilly Home | Privacy Policy

© 2007 O'Reilly Media, Inc.
Website: | Customer Service: | Book issues:

All trademarks and registered trademarks appearing on oreilly.com are the property of their respective owners.