Greasemonkey Hacks/Web Forms

From WikiContent

< Greasemonkey Hacks
Revision as of 23:10, 11 March 2008 by Docbook2Wiki (Talk)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search
Greasemonkey Hacks


Contents

Hacks 29–39: Introduction

The Web contains a fantastic array of services, applications, and interactive experiences. You can shop online, do research, check your email, and interact with other people on weblogs, message boards, and discussion forums. What do these all have in common? Forms.

Ask any web architect, and she'll tell you that web forms are woefully inadequate. At least two independent efforts are underway to radically overhaul the underlying technology of web forms. One of them might catch on; heck, the Web is big enough that both of them may catch on. But until then, we're stuck with the simple <form> element, a few <input> fields, and a Submit button.

Or are we?

Display Form Actions in a Tool Tip

Hover over a form's Submit button to see where the form will be submitted.

If you hover your cursor over a link, Firefox will show you the target URL in the status bar. But there is no similar functionality for forms. Clicking the Submit button could send you anywhere, and you won't know where until you're already there. This hack modifies web forms to display the form method (GET or POST) and action (target URL) in a tool tip when you hover the cursor over the form's Submit button.

The Code

This user script will run on all pages. The code itself is divided into three parts:

Find all the forms
This part is easy. Firefox maintains a global variable: document.forms.
Find each Submit button
Although unlikely, it is technically possible that a form could have more than one Submit button. For example, Google's home page has a form with two Submit buttons: Google Search and I'm Feeling Lucky.
Set the button's title
Pretty much any HTML element can have a title attribute, even form fields and buttons. Firefox will display the title as a tool tip when you hover over the element.

Tip

Don't make your user scripts more complicated than they need to be. Firefox maintains lots of lists for you: document.forms, document.images, document.links, document.anchors, document.applets, document.embeds, and document.styleSheets.

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

	// ==UserScript==
	// @name		  Display Form Action
	// @namespace	  http://diveintomark.org/projects/greasemonkey/
	// @description	  display form submission URL as tooltip of submit button
	// @include		  *
	// ==/UserScript==

	for (var i = document.forms.length - 1; i >= 0; i--) {
		var elmForm = document.forms[i];
		var snapSubmit = document.evaluate("//input[@type='submit']",
			elmForm, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		for (var j = snapSubmit.snapshotLength - 1; j >= 0; j--) {
			var elmSubmit = snapSubmit.snapshotItem(j);
			elmSubmit.title = (elmForm.method.toUpperCase() || 'GET') +
				' ' + elmForm.action;
		}
	}

Running the Hack

After installing the user script from Tools → Install This User Script, go to http://www.google.com and hover your cursor over the Google Search button. You will see a tool tip with the form action (GET) and the form submission URL (/search), as shown in Figure 4-1.

Figure 4-1. Google form submission tool tip

Google form submission tool tip

Hacking the Hack

One possible improvement on this hack would be to include the names of the submitted form fields in the tool tip:

	// ==UserScript==
	// @name		  Display Form Action
	// @namespace	  http://diveintomark.org/projects/greasemonkey/	
	// @description	  display form submission URL as tooltip of submit button	
	// @include		  *
	// ==/UserScript==

	for (var i = document.formsforms.length - 1; i >= 0; i--) {
		var elmForm = document.forms[i];
		var arElmFormFields = elmForm.getElementsByTagName('input');
		var arNames = new Array();
		for (var j = arElmFormFields.length - 1; j >= 0; j--) {
			var sName = arElmFormFields[j].name ||
				arElmFormFields[j].id;
			if (sName) {
				arNames.push(sName);
			}
		}
		var sFormFields = arNames.join(', ');
		var snapSubmit = document.evaluate("//input[@type='submit']",	
			elmForm, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		for (var j=snapSubmit.snapshotLength-1; j>=0; j--) {
			var elmSubmit = snapSubmit.snapshotItem(j);
			elmSubmit.title = (elmForm.method.toUpperCase() || 'GET') +
				' ' + elmForm.action + ' with ' + sFormFields;
		}
	}

If you want even more control over form submissions, check out POST Interceptor [Hack #45].

Show Hidden Form Fields

See what hidden information you're submitting to a site.

One of the features of HTML forms is the ability to include hidden form fields. If you view source on a page, you can see them, tucked away next to the visible form fields. Their presence can be completely innocuous, perhaps storing your previous input in a multipage progression of complex forms. They can also hold tracking information that the site developer uses to track your movements throughout the site. Whatever their purpose, it can be difficult to wade through the page source to see what the site is hiding from you.

This hack makes hidden form fields visible. And, as an added bonus, it makes them editable as well.

The Code

This user script runs on all pages. Most of the work is done by a single XPath query, which finds <input type="hidden"> elements. Then it's a simple matter of iterating through the <input> elements and changing them to <input type="text">, which makes them simultaneously visible and editable in one fell swoop.

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

	// ==UserScript==
	// @name			Display Hidden Form Fields
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		un-hide hidden form fields and make them editable
	// @include			*
	// ==/UserScript==
		   
	   var snapHidden = document.evaluate("//input[@type='hidden']",
		   document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	   for (var i = snapHidden.snapshotLength - 1; i >= 0; i--) {
		   var elmHidden = snapHidden.snapshotItem(i);
		   elmHidden.style.MozOutline = '1px dashed #666';
		   elmHidden.type = 'text';
		   elmHidden.title = 'Hidden field "' +
			   (elmHidden.name || elmHidden.id) + '"';
	   }

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.yahoo.com. This deceptively simple search form actually includes several hidden form fields, as shown in Figure 4-2.

Figure 4-2. Yahoo! home page with hidden form fields

Yahoo! home page with hidden form fields

To avoid confusion, the script adds a dashed border around form fields that were originally hidden.

Identify Password Fields

Decorate password fields with a special background pattern.

This hack improves the usability of forms by highlighting password fields with a special background. This makes it faster to fill out forms, because you don't need to worry about accidentally typing your password in the wrong box in clear text.

The script makes password fields less legible, but in practice, this doesn't matter much, because what you type is displayed only as asterisks or dots anyway.

The Code

This user script runs on all pages. It inserts a CSS rule that decorates any input field with class GM_PasswordField. The CSS rule sets a background image, encoded as a data: URL. The image is a 4 x 4 image, which is then tiled to fill the password input box. The resulting pattern alternates black, white, and transparent lines, so that it is recognizable on any background color. Finally, we use an XPath query to find all password fields and set their class attributes to link them to the special CSS rule.

Save the following user script as identify-password-fields.user.js:

	// ==UserScript==
	// @name		 Identify Password Fields
	// @namespace	 http://blog.monstuff.com/archives/cat_greasemonkey.html
	// @description	 Decorates password fields with a background pattern
	// @include		 *
	// ==/UserScript==
	 
	// based on code by Julien Couvreur
	// and included here with his gracious permission

	// add a password fieldsformsusing CSSCSS rule
	var rule = "input.GM_PasswordField { background-image: url(data:image/gif,"+
		"GIF89a%04%00%04%00%B3%00%00%FF%FF%FF%FF%FF%00%FF%00%FF%FF%00%00%00%FF"+
		"%FF%00%FF%00%00%00%FF%00%00%00%CC%CC%CC%FF%FF%FF%00%00%00%00%00%00%00"+
		"%00%00%00%00%00%00%00%00%00%00%00!%F9%04%01%00%00%09%00%2C%00%00%00%0"+
		"0%04%00%04%00%00%04%070I4k%A22%02%00%3B) }";

	 var styleNode = document.createElement("style");
	 styleNode.type = "text/css";
	 styleNode.innerHTML = rule;
	 document.getElementsByTagName('head')[0].appendChild(styleNode);

	 // find all formspassword fieldspassword fields and mark them with a class
	 var xpath = "//input[translate(@type,'PASSWORD','password')='password']";
	 var res = document.evaluate(xpath, document, null,
		  XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	 for (var inputIndex = 0; inputIndex < res.snapshotLength; inputIndex++) {
		  passwordInput = res.snapshotItem(inputIndex);
		  passwordInput.className += " GM_PasswordField";
	 }

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://login.passport.net, or any site with an account registration or login form. The password field in the login form will be rendered with a stripped background pattern, as shown in Figure 4-3.

Hacking the Hack

It turns out that CSS already supports a powerful way of doing field selection. And because Firefox supports web standards such as CSS so well, it is possible to avoid the XPath query altogether.

The revised user script in this section doesn't need to enumerate the password fields. Instead, it uses a single CSS selector, input[type='password'], to set the background image on all password fields.

Tip

Learn more about CSS selectors at http://www.xml.com/lpt/a/2003/06/18/css3-selectors.html.

Figure 4-3. Highlighted password field

Highlighted password field

Save the following user script as identify-password-fields2.user.js:

	// ==UserScript==
	// @name			Identify formspassword fieldsPassword Fields
	// @namespace		http://blog.monstuff.com/archives/cat_greasemonkey.html
	// @description		Decorates passwordspassword fieldspassword fields with a background pattern
	// @include			*
	// ==/UserScript==
	// based on code by Julien Couvreur
	// and included here with his gracious permission
	var rule = "input[type='formspassword fieldspassword'] { background-image: "	+
	  "url(data:image/gif,GIF89a%04%00%04%00%B3%00%00%FF%FF%FF" +
	  "%FF%FF%00%FF%00%FF%FF%00%00%00%FF%FF%00%FF%00%00%00%FF"	+
	  "%00%00%00%CC%CC%CC%FF%FF%FF%00%00%00%00%00%00%00%00%00"	+
	  "%00%00%00%00%00%00%00%00%00!%F9%04%01%00%00%09%00%2C%00" +
	  "%00%00%00%04%00%04%00%00%04%070I4k%A22%02%00%3B) }";

	 var styleNode = document.createElement("style");
	 styleNode.type = "text/css";
	 styleNode.innerHTML = rule;
	 document.getElementsByTagName('head')[0].appendChild(styleNode);

Uninstall the first script, install this one, and refresh http://login.passport.net. The effect is the same: the password field is highlighted. Hooray for CSS!

Julien Couvreur

Allow Password Remembering

Let the browser's password manager do its job.

I'm constantly filling out forms with the same data on different sites. Firefox tries to help by remembering past values and autocompleting form fields that it recognizes, but this doesn't always work. What's worse, some sites will use a special HTML attribute to tell the browser not to remember and autocomplete specific form fields. That's fine for sensitive information, such as social security numbers and credit card numbers, but sometimes I want my browser to remember my username or password, even if the site's developers think that's unsafe.

This hack removes that special HTML attribute (autocomplete="off") from all web forms and lets me decide whether I want to let Firefox store my form data and autocomplete it later.

Warning

This script lets you trade convenience for security. Firefox does not encrypt the form data that it stores on your computer. It's up to you to understand the risks of saving your personal information and weigh those risks against the convenience of autocompletion.

The Code

This user script runs on all pages. First, it defines a helper function that neutralizes any autocomplete attribute on an HTML element. Then, it iterates over each form and each of its fields, calling into the helper function for the cleaning.

Tip

This feature was first available in the form of a bookmarklet, a small chunk of JavaScript embedded in a URL and saved as a bookmark. Compared to user scripts, bookmarklets are more difficult to edit and debug, and they do not execute automatically when a page loads. But bookmarklets do not require any additional software. User scripts are a natural evolution of bookmarklets.

Save the following user script as allow-password-remembering.user.js:

	// ==UserScript==
	// @name			Allow formspassword rememberingPassword Remembering
	// @namespace		http://blog.monstuff.com/archives/cat_greasemonkey.html
	// @description		Removes autocomplete="off" attributes
	// @include			*
	// ==/UserScript==

	// based on code by Julien Couvreur
	// and included here with his gracious permission

	var allowAutoComplete = function(element) {
		var iAttrCount = element.attributes.length;
		for (var i = 0; i < iAttrCount; i++) {
			var oAttr = element.attributes[i];
			if (oAttr.name == 'autocomplete') {
				oAttr.value = 'on';
				break;
			 }
		 }
	 }

	 var formsforms = document.getElementsByTagName('form');
	 for (var i = 0; i < forms.length; i++) {
		  var form = forms[i];
		  var elements = form.elements;
		  allowAutoComplete(form);
		  for (var j = 0; j < elements.length; j++) {
			  allowAutoComplete(elements[j]);
		  }
	  }

Running the Hack

After installing the script (Tools → Install This User Script), go to http://login.passport.net and log in with your Passport account. (You can sign up for free if you don't have one.) When you submit the login form, Firefox will offer to remember your credentials for you, as shown in Figure 4-4.

Figure 4-4. password to remember

password to remember

If you select Yes, Firefox will prefill your account information the next time you log into any Microsoft Passport service, such as Hotmail or MSDN.

Julien Couvreur

Confirm Before Closing Modified Pages

Don't lose your changes in web forms when you accidentally close your browser window.

It's becoming more and more common for complex tasks to be performed on the Web. Of course, there is web-based email, and weblogging and wikis are also popular. Message boards are a great way to form a community, and there are many more online applications that are used every day by many people. One of the drawbacks, though, in not using a normal desktop program is losing that prompt, "Are you sure you wish to exit? You have unsaved work."

With Greasemonkey, we can restore this functionality and save the hassle caused by closing a window and losing your unsubmitted form data.

The Code

This script uses the power of the onbeforeunload event to catch the browser just before it moves off the page. When the page loads, the script finds all <textarea> elements and records the initial value of each one. Then, we register an onbeforeunload event handler to call our function that checks the current value of each <textarea>. If the current value differs from the previously recorded value, we display a dialog box to give the user the chance to save his work.

To make sure we don't interfere when the user actually submits the form, the script attaches an onsubmit event handler to all forms. This handler sets an internal flag to record that the user submitted the form and that we should not bother checking for unsubmitted data, since the user just submitted it!

Save the following user script as protect-textarea.user.js:

	// ==UserScript==
	// @name		  Protect Textarea
	// @namespace	  http://www.arantius.com/
	// @description	  Confirm before closing a web page with formsmodified pagesmodified textareas
	// @include		  *
	// @exclude		  http*://*mail.google.com/*
	// ==/UserScript==

	// based on code by Anthony Lieuallen
	// and included here with his gracious permission
	// http://www.arantius.com/article/arantius/protect+textarea/

	//indicator to skip handler because the unload is caused by form submission
	var _pt_skip=false;
	var real_submit = null;

	//find all textarea elements and record their original value
	var els=document.evaluate('//textarea',
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	for (var el=null, i=0; el=els.snapshotItem(i); i++) {
		var real_el = el.wrappedJSObject || el;
		real_el._pt_orig_value=el.value;
	}

	//if i>0 we found textareas, so do the rest
	if (i == 0) { return; }
	
	//this function handles the case where we are submitting the form,
	//in this case, we do not want to bother the user about losing data
	var handleSubmit = function() {
		_pt_skip=true;
		return real_submit();
	}
		
	//this function will handle the event when the page is unloaded and
	//check to see if any textareas have been modified
	var handleUnload = function() {
		if (_pt_skip) { return; }
		var els=document.getElementsByTagName('textarea');
		for (var el=null, i=0; el=els[i]; i++) {
			var real_el = el.wrappedJSObject || el;
			if (real_el._pt_orig_value!=el.value) {
				return 'You have formsmodified pagesmodified a textarea, and have not ' +
				'submitted the form.';
				}
			}
		}
		
		// trap form submit to set flag
		real_submit = HTMLFormElement.prototype.submit;
		HTMLFormElement.prototype.submit = handleSubmit;
		window.addEventListener('submit', handleSubmit, true);

		// trap unload to check for unmodified textareas
		unsafeWindow.onbeforeunload = handleUnload;

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.iupui.edu/~webtrain/tutorials/forms_sample.html . At the bottom of the form is a large box for entering additional comments. Enter some text, and then try to close the browser window. You will see a confirmation dialog, as shown in Figure 4-5.

Figure 4-5. Unsaved changes dialog

Unsaved changes dialog

If you press Cancel, you'll stay right where you are and can submit the form. If you press OK, the browser window will close.

Hacking the Hack

This hack can easily be extended to monitor all form fields, not just <textarea> elements. Instead of using document.getElementsByTagName to find only <textarea> elements, we can use an XPath expression to look for <input> elements, too.

	var els=document.evaluate('//textarea|//input',
	document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	for (var el=null, i=0; el=els.snapshotItem(i); i++) {
		…
	}

This will cause the script to protect all form fields containing text boxes, checkboxes, and radio buttons. It will not handle drop-down select boxes, though, because they function differently. It's more complicated than just adding //select to the XPath expression and examining the selectedIndex attribute of the <select> element, because some <select> boxes have multiple selections.

Anthony Lieuallen

Resize Text Input Fields with the Keyboard

Give yourself some more room to type in web forms.

Many sites now incorporate contributions from users, in the form of feedback, comments, or even direct editing. But the textarea experience can be pretty frustrating, in part, because the fields are often too small. Short of breaking out of the box entirely, this user script tries to relax that limitation. It allows you to stretch the boundaries of your input workspace.

Making web forms resizable can be implemented in different ways. One way lets you drag and drop the corner and sides of a textarea to resize them. Another method, illustrated in "Add a Text-Sizing Toolbar to Web Forms" [Hack #75], is to add zoom in and zoom out buttons on top of textareas.

One thing I didn't like about these solutions is that they interrupt my typing. They force my hand to move away from the keyboard. Instead, this hack makes use of keyboard shortcuts to do the resizing. For example, it lets you expand textareas vertically by pressing Ctrl-Enter, and horizontally by pressing Ctrl-spacebar.

The Code

This user script runs on all pages. It uses document.getElementsByTagName to list all the <textarea> elements and then instruments them. This consists of defining two helper methods for each <textarea> and wiring the field's keydown event to an event handler.

When a textarea is instrumented, the new helper functions that are created reference the textarea. Each function thus keeps access to the textarea it was created for, so it can modify the field's size when it is invoked.

In practice, when a key is pressed on a certain field, the corresponding textareaKeydown function gets called. It inspects the keyboard event, and if the right keyboard combination is pressed, it modifies the number of available columns or rows for the field. We also scroll the browser viewport so that the newly resized <textarea> element is still completely visible.

Tip

Functions in JavaScript can be returned like any other object. But function objects are a bit special, in that they keep a reference to the context in which they were created. When a function is created and returned, it captures the local variables or local scope that it could "see" when it was created. A function object that remembers the context in which it was created is called a closure. This capability is key to understanding event handling and, more generally, methods that use callbacks.

Save the following user script as textarea-resize.user.js:

	// ==UserScript==
	// @name			Textarea Resize
	// @namespace		http://blog.monstuff.com/archives/cat_greasemonkey.html
	// @description		Provides keyboard shortcuts for formsresizing text input fieldsresizing textareas
	// @include			*
	// ==/UserScript==

	// based on code by Julien Couvreur
	// and included here with his gracious permission

	var instrumentTextarea = function(textarea) {
		var centerTextarea = function() {
		  if (textarea.scrollIntoView) {
			  textarea.scrollIntoView(false);
		  } else {
			  textarea.wrappedJSObject.scrollIntoView(false);
		  }
	  };

	  var textareaKeydown = function(e) {
		if (e.shiftKey && e.ctrlKey && e.keyCode == 13) {
			// shift-ctrl-enter
			textarea.rows -= 1;
			centerTextarea();
		}
		else if (e.shiftKey && e.ctrlKey && e.keyCode == 32) {				  // shift-ctrl-space
			  formsresizing text input fieldstextarea.cols -= 1;
			  centerTextarea();
		}
		else if (e.ctrlKey && e.keyCode == 13) {
			  // ctrl-enter
			  if (textarea.offsetHeight < window.innerHeight - 40) {
				  textarea.rows += 1;
			  }
			  centerTextarea();
		}
		else if (e.ctrlKey && e.keyCode == 32) {
			 // ctrl-space
			 if (textarea.offsetWidth < window.innerWidth - 40) {
			     textarea.cols += 1;
			 }
			 centerTextarea();
		}
	};
			  
	textarea.addEventListener("keydown", textareaKeydown, 0);
}
		 
var textareas = document.getElementsByTagName("textarea");
for (var i = 0; i < textareas.length; i++) {
   instrumentTextarea(textareas[i]);
}

Running the Hack

After installing the script (Tools → Install This User Script), navigate to a site that has a textarea that is too small for your taste. I'll use one at http://www.htmlcodetutorial.com/forms/_TEXTAREA.html as an example.

Start typing in the form, as shown in Figure 4-6. To add extra rows to the input field, press Ctrl-Enter. To expand it horizontally (adding columns), press Ctrl-spacebar.

Figure 4-7 illustrates an expanded textarea. The script allows you to increase the size of the field even more, up to the size of your browser window. It also scrolls the page to bring the entire textarea into view, as needed.

If you want to shrink the textarea instead, use Shift-Ctrl-Enter and Shift-Ctrl-spacebar.

Julien Couvreur

Figure 4-6. A small textarea

A small textarea

Figure 4-7. A resized textarea

A resized textarea

Enter Textile Markup in Web Forms

Add a button to textareas to convert textile input to XHTML.

Textile is a minimalist markup language invented by Dean Allen for his weblog publishing system, Textpattern. Dean originally wrote a Textile-to-XHTML library in PHP. I quickly ported it to Python, and Jeff Minard took my Python version and ported it to JavaScript. Roberto De Almeida hacked together a Greasemonkey script to allow you to enter Textile markup in web forms by calling a CGI script on his server to do the conversion. Then, Phil Wilson improved on Roberto's work by integrating Jeff's JavaScript library, thus making the entire hack self-contained and free of external dependencies.

People ask why I love open source; this hack is why. This script was written by one person, then improved by a second person by integrating code written by a third person, who based his code on the work of a fourth person, who in turn based his code on the work of a fifth person. The end result of this collaboration is that you can write Textile markup in web forms and then convert it to XHTML with a single click. Everything is done locally, and then the form is submitted to the originating site as usual. There are no calls to third-party servers, and the originating site never has to know or care that you originally entered your comments in Textile format. That's just beautiful.

Tip

See a sample of Textile markup at http://textism.com/tools/textile/?sample=2.

The Code

This user script runs on all pages. The most complex part is the textile function, which converts Textile markup to XHTML. The rest of the script is straightforward: find all the <textarea> elements and create a button next to each textarea that calls the textile function.

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

		// ==UserScript==
		// @name		  Instant Textile
		// @namespace	  http://philwilson.org/
		// @description	  Allow Textile input in web forms
		// @include		  http*://*
		// ==/UserScript==

		// based on code by Phil Wilson, Robert De Almeida, and Jeff Minard
		// and included here with their gracious permission
		function formsTextile markuptextile(s) {
			var r = s;
			// quick tags first
			qtags = [['\\*', 'strong'],
				 ['\\?\\?', 'cite'],
				 ['\\+', 'ins'],
				 ['~', 'sub'],
				 ['\\^', 'sup'],
				 ['@', 'code']];
			for (var i=0;i<qtags.length;i++) {
				ttag = qtags[i][0]; htag = qtags[i][1];
				re = new RegExp(ttag+'\\b(.+?)\\b'+ttag,'g');
				r = r.replace(re,'<'+htag+'>'+'$1'+'</'+htag+'>');
			}

			// underscores count as part of a word, so do them separately
			re = new RegExp('\\b_(.+?)_\\b','g');
			r = r.replace(re,'<em>$1</em>');

			//jeff: so do dashes
			re = new RegExp('[\s\n]-(.+?)-[\s\n]','g');
			r = r.replace(re,'<del>$1</del>');

			// links
			re = new RegExp('"\\b(.+?)\\(\\b(.+?)\\b\\)":([^\\s]+)','g');
			r = r.replace(re,'<a href="$3" title="$2">$1</a>');
			re = new RegExp('"\\b(.+?)\\b":([^\\s]+)','g');
			r = r.replace(re,'<a href="$2">$1</a>');
			
			// images
			re = new RegExp('!\\b(.+?)\\(\\b(.+?)\\b\\)!','g');
			r = r.replace(re,'<img src="$1" alt="$2">');
			re = new RegExp('!\\b(.+?)\\b!','g');
			r = r.replace(re,'<img src="$1">');
			
			// block level formatting
		
			// Jeff's hack to show single line breaks as they should.
			// insert breaks - but you get some….stupid ones
			re = new RegExp('(.*)\n([^#\*\n].*)','g');
			r = r.replace(re,'$1<br />$2');
			// remove the stupid breaks.
			re = new RegExp('\n<br />','g');
			r = r.replace(re,'\n');
			
			lines = r.split('\n');
			nr = '';
			for (var i=0;i<lines.length;i++) {
				line = lines[i].replace(/\s*$/,'');
				changed = 0;
				if (line.search(/^\s*bq\.\s+/) != -1) {
				   line = line.replace(/^\s*bq\.\s+/,'\t<blockquote>') +
				   '</blockquote>';
				changed = 1;
			}
			
			// jeff adds h#.
			if (line.search(/^\s*h[1-6]\.\s+/) != -1) {
				re = new RegExp('h([1-6])\.(.+)','g');
				line = line.replace(re,'<h$1>$2</h$1>');
				changed = 1;
			}

			if (line.search(/^\s*\*\s+/) != -1) {
				line = line.replace(/^\s*\*\s+/,'\t<liu>') + '</liu>';
				changed = 1;
			} // * for bullet list; make up an liu tag to be fixed later
			if (line.search(/^\s*#\s+/) != -1) {
				line = line.replace(/^\s*#\s+/,'\t<lio>') + '</lio>';
				changed = 1;
			}

			// # for numeric list; make up an lio tag to be fixed later
			if (!changed && (line.replace(/\s/g,'').length > 0)) {
				line = '<p>'+line+'</p>';
			}
			lines[i] = line + '\n';
		}
		
		// Second pass to do lists
		inlist = 0;
			listtype = '';
		for (var i=0;i<lines.length;i++) {
			line = lines[i];
			if (inlist && listtype == 'ul' && !line.match(/^\t<liu/)) {
				line = '</ul>\n' + line;
				inlist = 0;
			}
			if (inlist && listtype == 'ol' && !line.match(/^\t<lio/)) {
				line = '</ol>\n' + line;
				inlist = 0;
			}
			if (!inlist && line.match(/^\t<liu/)) {
				line = '<ul>' + line;
				inlist = 1;
				listtype = 'ul';
			}
			if (!inlist && line.match(/^\t<lio/)) {
				line = '<ol>' + line;
				inlist = 1;
				listtype = 'ol';
			}
			lines[i] = line;
		}
		r = lines.join('\n');

		// finally, replace <li(o|u)> AND </li(o|u)> created earlier
		r = r.replace(/li[o|u]>/g,'li>');

		return r;
	}

	var arTextareas = document.getElementsByTagName("textarea");
	for (var i = 0; i < arTextareas.length; i++) {		
		var elmTextarea = arTextareas[i];
		var sID = elmTextarea.id;
		var elmButton = document.createElement("input");
		elmButton.type = "button";
		elmButton.value = "Textile it!";
		elmButton.addEventListener('click', function() {
			elmTextarea.value = textile(elmTextarea.value);
		}, true);
		elmTextarea.parentNode.insertBefore(elmButton,
			elmTextarea.nextSibling);
	}

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://simon.incutio.com/archive/2005/07/17/django. At the bottom of the page is a form for submitting comments. Enter some Textile markup, as shown in Figure 4-8.

Figure 4-8. Textile markup

Textile markup

Now, click the "Textile it!" button, and your Textile comment will be converted to valid XHTML, as shown in Figure 4-9.

Now you can submit your comment by clicking Preview Comment.

Hacking the Hack

This hack is cool, but it still requires an extra click to convert your comments from Textile to XHTML. What's that? An extra step, you say? Bah. Let's trap the form submission itself and automatically convert all the <textarea> elements.

Figure 4-9. Textile converted to XHTML

Textile converted to XHTML

This is trickier than it sounds. There are two ways to submit a web form: the user can click on an <input type="submit"> button, or the page can programmatically call the form.submit() method. When the user clicks a Submit button, Firefox fires an onsubmit event, which we can trap and insert our Textile conversion function before the browser submits the form data to the server. But if a script calls the form's submit method, Firefox never fires the onsubmit event. To trap form submission, in both cases, we need to actually override the submit method in the HTMLFormElement class.

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

		   // @name			Auto-Textile
		   // @namespace	http://philwilson.org/
		   // @description	Allow Textile input in web formsforms
		   // @include		http://www.example.com/
		   // ==/UserScript==

		   // Dear reader: I have omitted the textile() function here
		   // to save trees. Go hug a nearby tree, and then copy the
		   // textile() function from the textile.user.js script.
		   
		   function textile_and_submit(event) {
			   var form = event ? event.target : this;
			   
			   var arTextareas = form.getElementsByTagName('textarea');
			   for (var i = arTextareas.length - 1; i >= 0; i--) {
				   var elmTextarea = arTextareas[i];
				   elmTextarea.value = textile(elmTextarea.value);
			   }
			
			   form._submit();
			}
			
			// trap onsubmit event, for when user clicks an <input type="submit">
			window.addEventListener('submit', textile_and_submit, true);
			// override submit method, for when page script calls form.submit()
			HTMLFormElement.prototype._submit = HTMLFormElement.prototype.submit;
			HTMLFormElement.prototype.submit = formsTextile markuptextile_and_submit;

With these changes, any web form is automatically and transparently Textile-enabled. No extra buttons, no extra clicks. Of course, this breaks a large number of sites that weren't expecting XHTML markup, so running this script on every site would cause lots of virtual pain and suffering. The default @include parameter lists only an example site. You should add specific sites that expect XHTML comments.

Select Multiple Checkboxes

Toggle series of checkboxes at once in web forms with a Shift-click.

Web-based email is one of the great success stories when it comes to pure web-based applications. But most web mail sites are still more difficult to use than their desktop counterparts. One of the niceties that desktop programs offer is the ability to select multiple items in a list, by clicking the first item and then Shift-clicking another item, to select all the items in between. This hack brings this functionality to web-based applications, allowing you to click a checkbox (for example, to select a message in your web mail inbox) and then Shift-click another checkbox, to select all the checkboxes in between.

The Code

This user script was specifically tested on Hotmail, Yahoo! Mail, and Google Personalized Home Page. By default, it will run on all pages except Gmail, where it is known to cause problems. If you find that it interferes with other sites you use, you should add them to the "Excluded pages" list in the Manage User Scripts dialog.

The basic functionality is fairly straightforward. I do want to draw attention to one specific function: NSResolver. This function is passed as a parameter to the document.evaluate function to execute an XPath query. Firefox's XPath engine uses the NSResolver function to evaluate namespace prefixes in the XPath expression. In XHTML 1.0 and 1.1 pages served with a "Contenttype: application/xhtml+xml" HTTP header, all the elements on the page will be in the XHTML namespace, http://www.w3.org/1999/xhtml. The script checks for this condition by testing whether document.documentElement.namespaceURI is defined. If the namespace is defined, the script constructs an XPath expression with an xhtml: prefix, to find elements in the XHTML namespace. When Firefox's XPath engine evaluates the expression, it calls the NSResolver function to resolve the xhtml: prefix, and then searches for the requested elements in that namespace.

Since we know that XHTML is the only namespace we'll ever use, we cheat a little bit and always return the XHTML namespace from the NSResolver function. But if a page used multiple namespaces (for example, an XHTML document with embedded MathML or SVG data), we could check the prefix parameter in the NSResolver function and return the appropriate namespace for XHTML, MathML, SVG, or any other XML vocabulary we wanted to include in our XPath query.

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

		   // ==UserScript==
		   // @name			 Check Range
		   // @namespace	 http://squarefree.com/userscripts
		   // @description	 Multi-select a range of checkboxes
		   // @include		 *
		   // @exclude		 http*://mail.google.com/*
		   // ==/UserScript==

		   // based on code by Jesse Ruderman
		   // and included here with his gracious permission
		   
		   var elmCurrentCheckbox = null;
		   
		   function NSResolver(prefix) {
			   return 'http://www.w3.org/1999/xhtml';
		   }

		   function selectCheckboxRange(elmStart, elmEnd) {
			  var sQuery, elmLast;
	
			  if (document.documentElement.namespaceURI) {
				  sQuery = "//xhtml:input[@type='checkbox']";
			  } else {
			      sQuery = "//input[@type='checkbox']";
			  }

			  var snapCheckboxes = document.evaluate(sQuery, document, NSResolver,
				  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

			  var i;
			  for (i = 0; i < snapCheckboxes.snapshotLength; i++) {
				  var elmCheckbox = snapCheckboxes.snapshotItem(i);
				  if (elmCheckbox == elmEnd) {
				  elmLast = elmStart;
				  break;
				  }
				  if (elmCheckbox == elmStart) {
				  elmLast = elmEnd;
				  break;
				  }
			   }
				   
			   // note: intentionally re-using counter variable i
			   for (; (elmCheckbox = snapCheckboxes.snapshotItem(i)); ++i) {
				if (elmCheckbox != elmStart &&
				elmCheckbox != elmEnd &&
				elmCheckbox.checked != elmStart.checked) {
				// Fire are onclick event instead of modifying the checkbox's
				// value directly, fire an onclick event. Yahoo! Mail and
				// Google Personalize have onclick handlers on their
				// checkboxes. This will also trigger an onchange event,
				// which some sites rely on.
				var event = document.createEvent("MouseEvents");
				event.initEvent("click", true, false);
				elmCheckbox.dispatchEvent(event);
				 }
				 if (elmCheckbox == elmLast) { break; }
				 }
			 }

			 function handleChange(event) {
				var elmTarget = event.target;
				if (isCheckbox(elmTarget) &&
				   (event.button == 0 || event.keyCode == 32)) {
				if (event.shiftKey && elmCurrentCheckbox) {
				selectCheckboxRange(elmCurrentCheckbox, elmTarget);
				}
				elmCurrentCheckbox = elmTarget;
				 }
			 }

			 function isCheckbox(elm) {
			 return (elm.tagName.toUpperCase()=="INPUT" && elm.type=="checkbox"); 
			 }

			 document.documentElement.addEventListener("keyup", handleChange, true);
			 document.documentElement.addEventListener("click", handleChange, true);

Running the Hack

After installing the user script (Tools → Install This User Script), log into http://mail.yahoo.com if you have a Yahoo! Mail account. If you have multiple messages in your Yahoo! Mail inbox, select the checkbox next to the first one and then Shift-click the checkbox next to one farther down the list. All the checkboxes in between will be automatically selected, as shown in Figure 4-10.

You can now delete all the messages, mark them as spam, or mark them as read, just as if you had selected each message individually.

Figure 4-10. Selecting multiple messages in Yahoo! Mail

Selecting multiple messages in Yahoo! Mail

The same works in reverse. Clear all the checkboxes by clicking the Clear All link. Select a checkbox halfway down the list, and then Shift-click on the checkbox next to the first message. Again, all the checkboxes in between will be automatically selected.

You can also use the hack entirely with the keyboard. If you press the Tab key enough times, you will see the focus move around the page and eventually on to the first checkbox in the inbox. Press the spacebar to select the message, and then tab several times to set focus on a message halfway down the list. Press Shift-spacebar to select the message, and all the checkboxes in between will be selected.

Keep Track of Secure Site Passwords

Generate random passwords for every site based on a master password.

Everyone has too many passwords to remember. Every site—from Expedia to Amazon.com to Gmail to individual blogs and mailing lists—has its own system. Some services—such as Microsoft's Passport, Google's Blogger, and SixApart's TypeKey—have tried to stem the tide by providing a cross-site login system. But even these are proliferating at an alarming rate. Most people eventually just give up and use one password everywhere. Some people use a "secure" password for sensitive sites like online banking and e-commerce sites, and an "insecure" password for mailing lists and blogs. All of these systems are doomed to failure.

What we really need is a personalized system of generating passwords locally and retrieving them on demand. Mac OS X has the Keychain application, but it works only on Mac OS X. Firefox has its Password Manager, but it doesn't store the passwords securely, and it works only on sites that allow the browser to remember passwords in the first place. (But see "Allow Password Remembering" [Hack #32] for a way around that.)

This hack defines a local master password that you can enter to generate a random password for each web site you visit. It never stores the master password on disk; you simply enter it whenever you need to log into a web site. So even if someone steals your laptop, she won't be able to access any of your stored passwords, because you haven't stored them anywhere.

The Code

This user script runs on all pages. The first half of the code is taken up with the MD5 hash algorithm, which the script uses to generate each password. The long mpwd_getHostname function is devoted to determining the portion of the current domain, minus the country-specific top-level domain name. This means that you can reuse the same site-specific password on http://www.amazon.com and http://www.amazon.co.uk.

The rest of the script checks for password fields on the current page and adds a Master password field to each form to allow you to enter your master password. If you enter it correctly, the script fills the original password field with your random site-specific password, or generates a new one and stores it locally.

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

		   // ==UserScript==
		   // @name		   Password Composer
		   // @namespace   http://joe.lapoutre.com/BoT/Javascript/
		   // @description  Generate site specific passwords based on a master password
		   // @include	   *
		   // @version	   1.08
		   // ==/UserScript==

		   // based on code by Johannes le Poutré and others
		   // and included here with their gracious permission

		   var clearText = false; // show generated passwds in cleartext
		   var topDomain = false; // use top domain instead of full host
		   function errLog(msg) {
			   if (typeof(GM_log == "function")) {
				   GM_log(msg);
			   } else {
				   window.status = msg;
			   }
		   }
		   
		   function hex_md5(s) {
			   return binl2hex(core_md5(str2binl(s), s.length * 8));
		   }
		   function core_md5(x, len) {
			   x[len >> 5] |= 0x80 << ((len) % 32);
			   x[(((len + 64) >>>9) << 4) + 14] = len;
			   var a = 1732584193;
			   var b = -271733879;
			   var c = -1732584194;
			   var d = 271733878;
			   for (var i = 0; i < x.length; i += 16) {
				   var olda = a;
				   var oldb = b;
				   var oldc = c;
				   var oldd = d;
				   a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);
				   d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
				   c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
				   b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
				   a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
				   d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
				   c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
				   b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
				   a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
				   d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
				   c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
				   b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
				   a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
				   d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
				   c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
				   b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);
				   a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
				   d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
				   c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
				   b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);
				   a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
				   d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
				   c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
				   b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
				   a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
				   d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
				   c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
				   b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
				   a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
				   d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
				   c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
				   b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
				   a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
				   d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
				   c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
				   b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
				   a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
				   d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
				   c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
				   b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
				   a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
				   d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);
				   c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
				   b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
				   a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
				   d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
				   c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
				   b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);
				   a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);
				   d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
				   c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
				   b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
				   a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
				   d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
				   c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
				   b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
				   a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
				   d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
				   c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
				   b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
				   a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
				   d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
				   c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
				   b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);
				   a = safe_add(a, olda);
				   b = safe_add(b, oldb);
				   c = safe_add(c, oldc);
				   d = safe_add(d, oldd);
				}
				return Array(a, b, c, d);
			} 
			function md5_cmn(q, a, b, x, s, t) {
				return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),
			b);
		}
		function md5_ff(a, b, c, d, x, s, t) {
			return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
		}
		function md5_gg(a, b, c, d, x, s, t) {
			return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
		}
		function md5_hh(a, b, c, d, x, s, t) {
			return md5_cmn(b ^ c ^ d, a, b, x, s, t);
		}
		function md5_ii(a, b, c, d, x, s, t) {
			return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
		}
		function safe_add(x, y) {
			var lsw = (x & 0xFFFF) + (y & 0xFFFF);
			var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
			return (msw << 16) | (lsw & 0xFFFF);
		}
		function bit_rol(num, cnt) {
			return (num << cnt) | (num >>>(32 - cnt));
		}
		function str2binl(str) {
			var bin = Array();
			var mask = (1 << 8) - 1;
			for (var i = 0; i < str.length * 8; i += 8) {
				bin[i >> 5] |= (str.charCodeAt(i / 8) & mask) << (i % 32);
			}
			return bin;
		}
		function binl2hex(binarray) {
			var hex_tab = '0123456789abcdef';
			var str = '';
			for (var i = 0; i < binarray.length * 4; i++) {
				str+=hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xF)+
				hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8)) & 0xF);
			}
			return str;
		}

		function mpwd_getHostname() {
			var re = new RegExp('https*://([^/]+)');
			var url = document.location.href.toLowerCase();
			var host = url.match(re)[1];
			// look at minimum domain instead of host
			// see http://labs.zarate.org/passwd/
			if (topDomain) {
				host = host.split('.');
				if (host[2] != null) {
				s = host[host.length-2] + '.' + host[host.length-1];
				domains='ab.ca|ac.ac|ac.at|ac.be|ac.cn|ac.il|ac.in|ac.jp|'+
		'ac.kr|ac.nz|ac.th|ac.uk|ac.za|adm.br|adv.br|agro.pl|ah.cn|aid.pl|alt'+
		'.za|am.br|arq.br|art.br|arts.ro|asn.au|asso.fr|asso.mc|atm.pl|auto.p'+
		'l|bbs.tr|bc.ca|bio.br|biz.pl|bj.cn|br.com|cn.com|cng.br|cnt.br|co.ac'+
		'|co.at|co.il|co.in|co.jp|co.kr|co.nz|co.th|co.uk|co.za|com.au|com.br'+
		'|com.cn|com.ec|com.fr|com.hk|com.mm|com.mx|com.pl|com.ro|com.ru|com.'+
		'sg|com.tr|com.tw|cq.cn|cri.nz|de.com|ecn.br|edu.au|edu.cn|edu.hk|edu'+
		'.mm|edu.mx|edu.pl|edu.tr|edu.za|eng.br|ernet.in|esp.br|etc.br|eti.br'+
		'|eu.com|eu.lv|fin.ec|firm.ro|fm.br|fot.br|fst.br|g12.br|gb.com|gb.ne'+
		't|gd.cn|gen.nz|gmina.pl|go.jp|go.kr|go.th|gob.mx|gov.br|gov.cn|gov.e'+
		'c|gov.il|gov.in|gov.mm|gov.mx|gov.sg|gov.tr|gov.za|govt.nz|gs.cn|gsm'+
		'.pl|gv.ac|gv.at|gx.cn|gz.cn|hb.cn|he.cn|hi.cn|hk.cn|hl.cn|hn.cn|hu.c'+
		'om|idv.tw|ind.br|inf.br|info.pl|info.ro|iwi.nz|jl.cn|jor.br|jpn.com|'+
		'js.cn|k12.il|k12.tr|lel.br|ln.cn|ltd.uk|mail.pl|maori.nz|mb.ca|me.uk'+
		'|med.br|med.ec|media.pl|mi.th|miasta.pl|mil.br|mil.ec|mil.nz|mil.pl|'+
		'mil.tr|mil.za|mo.cn|muni.il|nb.ca|ne.jp|ne.kr|net.au|net.br|net.cn|n'+
		'et.ec|net.hk|net.il|net.in|net.mm|net.mx|net.nz|net.pl|net.ru|net.sg'+
		'|net.th|net.tr|net.tw|net.za|nf.ca|ngo.za|nm.cn|nm.kr|no.com|nom.br|'+
		'nom.pl|nom.ro|nom.za|ns.ca|nt.ca|nt.ro|ntr.br|nx.cn|odo.br|on.ca|or.'+
		'ac|or.at|or.jp|or.kr|or.th|org.au|org.br|org.cn|org.ec|org.hk|org.il'+
		'|org.mm|org.mx|org.nz|org.pl|org.ro|org.ru|org.sg|org.tr|org.tw|org.'+
		'uk|org.za|pc.pl|pe.ca|plc.uk|ppg.br|presse.fr|priv.pl|pro.br|psc.br|'+
		'psi.br|qc.ca|qc.com|qh.cn|re.kr|realestate.pl|rec.br|rec.ro|rel.pl|r'+
		'es.in|ru.com|sa.com|sc.cn|school.nz|school.za|se.com|se.net|sh.cn|sh'+
		'op.pl|sk.ca|sklep.pl|slg.br|sn.cn|sos.pl|store.ro|targi.pl|tj.cn|tm.'+
		'fr|tm.mc|tm.pl|tm.ro|tm.za|tmp.br|tourism.pl|travel.pl|tur.br|turyst'+
		'yka.pl|tv.br|tw.cn|uk.co|uk.com|uk.net|us.com|uy.com|vet.br|web.za|w'+	
		'eb.com|www.ro|xj.cn|xz.cn|yk.ca|yn.cn|za.com';
				domains=domains.split('|');
				for(var i=0; i<domains.length; i++) {
				if (s==domains[i]) {
				s=host[host.length-3]+'.'+s;
				break;
				}
				}
				} else {
				s = host.join('.');
				}
				return s;
			} else {
				// no manipulation (full host name)
				return host;
			}
		}
		
		// generate the random password generation and storagepassword and populate original form
		function mpwd_doIt() {
			if (!mpwd_check_password()) { return; }
			var master = document.getElementById('masterpwd').value;
			var domain = document.getElementById('mpwddomain').value.toLowerCase();
			// remove panel before messing with passwd fields
			mpwd_remove();
			if (master != '' && master != null) {
				var i=0, j=0, p=hex_md5(master+':'+domain).substr(0,8);
				var inputs = document.getElementsByTagName('input');
				for(i=0;i<inputs.length;i++) {
				var inp = inputs[i];
				if(inp.getAttribute('type') == 'password') {
				inp.value=p;
				if (clearText) {
				inp.type = 'text';
				var cl = inp.getAttribute("class") || "";
				// hack to mark passwd fields by setting class name
				// intentde to find them on a second pass, if
				// type is modified to text
				if (cl.indexOf("mpwdpasswd") == -1) {
				inp.setAttribute("class", cl + " mpwdpasswd");
				}
				}
				// inp.focus();
				} else if(inp.getAttribute('type') == 'text') {
				var nm = inp.getAttribute('name').toLowerCase();
				var cl = inp.getAttribute("class") || "";
				// field named something like passwd or class mpwdpasswd
				if (nm.indexOf('random password generation and storagepassword')!=-1 ||
				nm.indexOf('passwd')!=-1 ||
				cl.indexOf("mpwdpasswd") != -1) {
				inp.value=p;
				if (! clearText) inp.type = 'password';
				// inp.focus();
				}
				}
			}
			// give focus to first password field
			getPwdFld().focus();
		}
	};

	// check for multiple passwd fields per form (e.g. 'verify passwd')
	function hasMultiplePwdFields() {
		// find any form that has 2+ password fields as children
		// note literal '>' char in xpath expression!
		var xpres = document.evaluate(
			"count(//form[count(//input[@type='password']) > 1])",
			document, null, XPathResult.ANY_TYPE, null);
		return(xpres.numberValue > 0);
	}
	// find first password field
	function getPwdFld() {
		var L = document.getElementsByTagName('input');
		for (var i = 0; i < L.length; i++) {
			var nm, tp, cl;
			try { nm = L[i].getAttribute("name") || ""; } catch(e) { };
			try { tp = L[i].getAttribute("type") || ""; } catch(e) { };
			try { cl = L[i].getAttribute("class") || ""; } catch(e) { };
			if ((tp == "password") ||
				(tp == "text" && nm.toLowerCase().substring(0,5) == "passw") ||
				(cl.indexOf("mpwdpasswd") > -1)) {
				return L[i];
			}
		}
		return null;
	}
	function mpwd_remove() {
		var body = document.getElementsByTagName('body')[0];
		body.removeChild(document.getElementById('mpwd_bgd'));
		body.removeChild(document.getElementById('mpwd_panel'));
	}
	function mpwd_keyup(e) {
		mpwd_check_random password generation and storagepassword();
		if (e.keyCode == 13 || e.keyCode == 10) {
			mpwd_doIt();
		} else if (e.keyCode == 27) {
			mpwd_remove();
		}
	}
	function mpwd_check_password() {
		var pwd = document.getElementById('masterpwd');
		var pwd2 = document.getElementById('secondpwd');
		if (!pwd2) return 1;
		if (pwd.value != pwd2.value && pwd2.value != '') {
			pwd2.style.background='#f77';
			pwd2.style.borderColor='red';
			return 0;
		} else {
			pwd2.style.background = 'white';
			pwd2.style.borderColor='#777';
			return 1;
		}
	}
	function mpwd_panel(event) {
		var pwdTop = 0;
		var pwdLeft = 0;
		if (document.getElementById('mpwd_panel')) {
			mpwd_remove();
			return;
		}
		try {
			var obj = getPwdFld();
			if (obj.offsetParent) {
				while (obj.offsetParent) {
				pwdTop += obj.offsetTop;
				pwdLeft += obj.offsetLeft;
				obj = obj.offsetParent;
				}
			}
		} catch (e) {
		  pwdTop = 10;
		  pwdLeft = 10;
		}
		// full document width and height as rendered in browser:
		var html = document.getElementsByTagName('html')[0];
		var pag_w = parseInt(document.defaultView.getComputedStyle(html,
			'').getPropertyValue('width'));
		var pag_h = parseInt(document.defaultView.getComputedStyle(html,
			'').getPropertyValue('height'));

		var div = document.createElement('div');
		div.style.color='#777';
		div.style.padding='5px';
		div.style.backgroundColor='white';
		div.style.border='1px solid black';
		div.style.borderBottom='3px solid black';
		div.style.borderRight='2px solid black';
		div.style.MozBorderRadius='10px';
		div.style.fontSize='9pt';	
		div.style.fontFamily='sans-serif';
		div.style.lineHeight='1.8em';
		div.style.position='absolute';
		div.style.width='230px';
		// keep panel at least 10 px away from right page edge
		div.style.left = ((250 + pwdLeft > pag_w)? pag_w - 250 : pwdLeft) +
	'px';
		div.style.top = pwdTop + 'px';
		div.style.zIndex = 9999; // make sure we're visible/on top
		div.setAttribute('id', 'mpwd_panel');
		div.appendChild(document.createTextNode('Master random password generation and storagepassword: '));

		var icnShow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAA'+
	'AAMCAIAAADZF8uwAAAAkUlEQVR4nGL4TwRgwCWxc%2BfOU6dOQRUZowKI6N%2B%2Ff93c'+
	'3Ly8vBCKMI3Ztm1bZ2dnenr65cuXcSoKCwt7%2BPDh4cOHs7KysCt6%2Ffo1Pz%2B%2Fs'+
	'7Ozk5MTkPH582csiiZMmDBlyhQIu6amZubMmVgUGRkZvXr1CsK%2Bffu2oaEhdjdBAFyK'+
	'aEV4AFQRLmOQAQAAAP%2F%2FAwB27VC%2BrCyA0QAAAABJRU5ErkJggg%3D%3D';
		var icnHide = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAA'+
	'AAMAgMAAAArG7R0AAAADFBMVEX%2F%2F%2F%2FMzMxmZmYzMzM7z8wMAAAAJ0lEQVQImW'+
	'NgAIMEhv%2FH0hgOMEYwHGAD0kD%2BAaAoBBuA8f%2F%2FH0AKARI5DD%2FY1kZdAAAAA'+
	'ElFTkSuQmCC';
		var show = document.createElement('img');
		show.setAttribute('src', (clearText) ? icnShow : icnHide);
		show.setAttribute('id', "icnShow");
		show.setAttribute('title', 'Show or hide generated password');
		show.style.paddingRight = '4px';
		show.style.display='inline'; // some stupid web sitessecure site password generation and storagesites set this to block
		show.style.cursor = 'pointer';
		show.addEventListener('click', function(event) {
			clearText = !clearText;
			document.getElementById("icnShow").setAttribute('src',
				(clearText) ? icnShow : icnHide);
			document.getElementById('masterpwd').focus();
		}, true);
		div.appendChild(show);

		var pwd = document.createElement('input');
		pwd.style.border='1px solid #777';
		pwd.setAttribute('type','password');
		pwd.setAttribute('id','masterpwd');
		pwd.style.width = '100px';
		pwd.style.fontSize='9pt';
		pwd.style.color='#777';
		// fire action if RETURN key is typed
		div.appendChild(pwd);
		div.appendChild(document.createElement('br'));
		if (hasMultiplePwdFields()) {
			// only of a 'verify field' is on original page
			div.appendChild(document.createTextNode('Check password: '));
		var pwd2 = document.createElement('input');
		pwd2.setAttribute('type','random password generation and storagepassword');
		pwd2.setAttribute('id','secondpwd');
		pwd2.style.width = '100px';
		pwd2.style.color='#777';
		pwd2.style.border='1px solid #777';	
		pwd2.style.fontSize='9pt';
		div.appendChild(pwd2);
		div.appendChild(document.createElement('br'));
	}
	
	div.appendChild(document.createTextNode('Domain: '));
	
	var subicn = document.createElement('img');
	subicn.setAttribute('src', 'data:image/png;base64,iVBORw0KGgoAAAAN'+
  'SUhEUgAAAAkAAAAJCAAAAADF%2BlnMAAAAGUlEQVR42mNogQGGlv8QgIvFAAL%2FCaqDA'+
  'QCbtDxVGHcjrgAAAABJRU5ErkJggg%3D%3D');
	subicn.setAttribute('id', "icnSubdom");
	subicn.setAttribute('title', 'Toggle use sub domain');
	subicn.style.display='inline';
	subicn.style.paddingRight = '4px';
	subicn.style.cursor = 'pointer';
	subicn.addEventListener('click', function(event) {
		toggleSubdomain();
		document.getElementById('masterpwd').focus();
	}, true);
	div.appendChild(subicn);

	var domn = document.createElement('input');
	domn.setAttribute('type','text');
	domn.setAttribute('value', mpwd_getHostname());
	domn.setAttribute('id','mpwddomain');	
	domn.setAttribute('title','Edit domain name for different password');
	domn.style.width = '150px';
	domn.style.border = 'none';
	domn.style.fontSize='9pt';
	domn.style.color='#777';
	div.appendChild(domn);

	div.addEventListener('keyup', mpwd_keyup, true);

	var bgd = document.createElement('div');
	bgd.setAttribute('id','mpwd_bgd');
	bgd.style.position='absolute';
	bgd.style.top='0px';
	bgd.style.left='0px';
	bgd.style.backgroundColor='black';
	bgd.style.opacity='0.35';
	bgd.style.height = pag_h + 'px';
	bgd.style.width = pag_w + 'px';
	bgd.style.zIndex='9998';
	bgd.addEventListener('click', mpwd_remove, true);
		var body = document.getElementsByTagName('body')[0];
		body.appendChild(bgd);
		body.appendChild(div);
		setTimeout("document.getElementById('masterpwd').focus();", 333);
		initSubdomainSetting();
	};

	// Setting: use sub domain
	function initSubdomainSetting() {
		if (typeof(GM_getValue) == 'function') {
			topDomain = GM_getValue('topDomain', false);
		}
		updateSubDomainSetting();
	}

	function toggleSubdomain() {
		topDomain = !topDomain;
		if (typeof(GM_setValue) == 'function') {
			GM_setValue('topDomain', topDomain);
		}
		updateSubDomainSetting();
	}
	
	function updateSubDomainSetting() {
		var icnPlus = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAA'+
	'AAJCAAAAADF%2BlnMAAAAHUlEQVR42mNogQGGlv8QAGExYLAYQACnLFwvDAAA6Fk4WdfT'+
	'%2FgAAAAAASUVORK5CYII%3D';
		var icnMin = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAA'+
	'AJCAAAAADF%2BlnMAAAAGUlEQVR42mNogQGGlv8QgIvFAAL%2FCaqDAQCbtDxVGHcjrgA'+
	'AAABJRU5ErkJggg%3D%3D';
		 document.getElementById("icnSubdom").setAttribute('src',
			(topDomain) ? icnMin : icnPlus);
		document.getElementById("mpwddomain").setAttribute('value',
			 mpwd_getHostname());
	}

	function mpwd_launcher() {
		// image 12px
		var bullet = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAA'+
	'AMCAYAAABWdVznAAAAX0lEQVR4nGL4%2F%2F8%2FAykYwkhL%2B08IR0VFIWkAUn%2F%2'+
	'B%2FMGJoRqysGoASRKtAWw9SOjMGRTN%2BG0AKgYLQzUT1AC3iVgNcGdBMVE2EOVpQhhV'+
	'AxCDBAhhFA3EYgAAAAD%2F%2FwMAKhyYBtU1wpoAAAAASUVORK5CYII%3D';
		var pwdTop = 0;
		var pwdLft = 0;
		var obj;
		try {
			obj = getPwdFld();
			if (obj.offsetParent) {
				while (obj.offsetParent) {
				pwdTop += obj.offsetTop;
				pwdLft += obj.offsetLeft;
				obj = obj.offsetParent;
				}
				}
				} catch (e) {
				    pwdTop = 10;
				pwdLft = 10;
				}
				// return if no passwd field is found
				if (! obj) return;
				var bull = document.createElement('img');
				bull.style.position='absolute';
				bull.style.top = pwdTop + 'px';
				bull.style.left = (pwdLft - 12) + 'px';
				bull.setAttribute('src', bullet);
				bull.setAttribute('title', 'Open random password generation and storagePassword Composer');
				bull.style.cursor = 'pointer';
				bull.addEventListener('click', mpwd_panel, true);
				bull.style.zIndex = 9999;
				document.getElementsByTagName('body')[0].appendChild(bull);
			}
			
			mpwd_launcher();
			// add menu command to manually launch passwd composer
			if (typeof(GM_registerMenuCommand == 'function')) {
				GM_registerMenuCommand("Show Password Composer", mpwd_panel);
			}

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.flickr.com and click "Sign up" to register a new account. In the registration form, you will see a small icon next to the password field titled "Open Password Composer," as shown in Figure 4-11.

Figure 4-11. Password Composer active

Password Composer active

Click the icon to open the Password Composer, and you have a chance to enter your master password, as shown in Figure 4-12.

Figure 4-12. Entering a master password

Entering a master password

Enter a password in the box, press Enter, and you will go back to the original page with the password field filled with a generated password.

You have the option of using this password for this entire site or just for a specific subdomain. This is especially useful on blogging sites such as Blogspot or TypePad, where each subdomain is really a different site owned by a different person. You can choose a different random password for each subdomain, or you can choose to share a single password across all subdomains.

The script is also smart enough to handle web forms that contain multiple password fields, such as sites that tell you to enter a password and then immediately enter it again in a separate field to make sure you didn't mistype it. The script autofills both password fields once you type your master password.

Automatically Log into Web Mail and Other Sites

Automate the hassle of using web-based login forms.

Firefox has an option to remember usernames and passwords in login forms. But even when it remembers your login and autofills the form, you're still left with one last click to submit the form and log into the site. This is definitely an improvement over needing to remember the password you used for each site, but over time, it can still be annoying, since most sites will force you to reenter your password once or twice a week. This hack works in conjunction with Firefox's autofill capabilities to autosubmit these autofilled login forms.

The Code

This user script runs on all pages. It looks for the first form that contains a text field marked as a password field (<input type="password">), and checks whether the password field contains an autofilled value. If so, it simulates a click on the form's Submit button to automatically log into the site.

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

			// ==UserScript==
			// @name		  AutoLoginJ
			// @namespace	  http://www.squarefree.com/userscripts
			// @description	  Automatically submit autofilled login forms
			// @include		  *
			// ==/UserScript==
			
			// based on code by Jesse Ruderman
			// and included here with his gracious permission
			// http://www.squarefree.com/userscripts/code samplesautologinj.user.jsautologinj.user.js

			function submitFirstPasswordForm() {
				for (var elmForm, i=0; elmForm=document.forms[i]; ++i) {
				var numPasswordElements = 0;
				for (var j=0; elmFormElement=elmForm.elements[j]; ++j)
				if (elmFormElement.type == "password" &&
					elmFormElement.value &&
					elmFormElement.value.toLowerCase() != "password") {
				++numPasswordElements;
				}
				if (numPasswordElements != 1) { continue; }
				/*
				 * The obvious way to submit a login form is form.submit().	
				 * However, this doesn't work with some forms, such as
				 * the Google AdWords login, because they do stuff
				 * in the onclick handler of the submit button. So we
				 * need to find the submit button and simulate a click.
				 */
				var elmSubmit = document.evaluate(".//input[@type='image']",
				elmForm, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
				null).singleNodeValue;
				if (!elmSubmit) {
				elmSubmit = document.evaluate(".//input[@type='submit']",
				elmForm, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
				null).singleNodeValue;
				   }
				   if (!elmSubmit) { continue; }
				   /*
				* Give a visual indication that we're logins to web sitesauto-submitting the
				* form, then simulate a click on the submit button.
				*/
				   elmSubmit.focus();
				   elmSubmit.style.MozOutline = "2px solid purple";
				   elmSubmit.click();
				}
			}

			window.addEventListener("load", function() {
				/*
				 * Using setTimeout to give Firefox's password manager a chance	
				 * to autofill the form.
				 */
				setTimeout(submitFirstPasswordForm, 0);
			}, false);

Running the Hack

For this hack, I will use Passport as an example, but the user script works on any site that uses a form-based login. This hack works only if you have told Firefox to remember your password. If you have previously told Firefox not to save your Yahoo! Mail password, go to Tools → Options → Privacy → Saved Passwords → View Saved Passwords → Passwords Never Saved and remove Passport from the list of sites for which Password Manager will never save login information.

Furthermore, many sites use a proprietary HTML attribute to tell your browser not to offer the choice of remembering your login information. Such sites will defeat the purpose of this user script, which works only after the browser autofills the login form with saved information. You can use "Allow Password Remembering" [Hack #32] to fight back against such sites and allow your browser to remember your login information, and then use this script to automatically log in after the login form is autofilled.

Go to http://login.passport.net/ and log into Microsoft Passport. Firefox will ask you whether you want to save the username and password for this site, as shown in Figure 4-13. Click Yes to remember your Passport login information.

Now log out of Passport and install the user script from Tools → Install This User Script. Revisit or refresh http://login.passport.net/. You should see the login page, and then Firefox will automatically fill in your saved Passport username and password. Shortly after, the script will automatically submit the login form.

Figure 4-13. Remember this password?

Remember this password?

Build Calendar Events

Use a graphical interface to construct HCalendar event markup.

HCalendar is an emerging microformat for displaying event information in XHTML. An HCalendar event can be displayed as is in any web page; the raw data itself is already valid XHTML (and HTML). But the data is structured enough that it can be processed by scripts without any complicated heuristics or loss of data.

Tip

Learn more about microformats at http://microformats.org.

This hack adds a complete HCalendar interface for entering event information in web forms.

The Code

This user script runs on all pages. You can change the @include parameter to run only on HCalendar-aware scheduling sites. It adds a link before each <textarea> element to show the HCalendar interface. When you click the hCal link, it replaces the <textarea> with a subform that contains all the common HCalendar fields, such as the event summary, start and end times, and a URL for more information. Once you submit the HCalendar form, the script constructs the HCalendar markup for you and inserts it into the original <textarea>.

Save the following user script as magic-hcalendar.user.js:

		// ==UserScript==
		// @name		  Magic hCalendar Microformatter
		// @namespace	  http://www.decafbad.com/
		// @description	  Enhances text areas with hCalendar microformat tools	
		// @include		  *
		// ==/UserScript==
		
		// based on code by Les Orchard
		// and included here with his gracious permission
		
		function HCalendarCreator(editor_id, callback) {
			this.editor_id = editor_id;
			this.callback = callback;
		}
		
		HCalendarCreator.prototype = {
			init: function() {
				// Get the editor and the form.
				this.editor = document.getElementById(this.editor_id);
				this.frm = this.editor.getElementsByTagName("form")[0];
			
				// Wire up the build & insert button with the callback.
				var _this = this;
				this.frm.elements.namedItem('build').addEventListener(
				'click', function(event) {
				var resultstr = _this.buildContent();
				
				if (_this.frm.elements.namedItem('compact').checked) {
				var regex = /\n/gi;
				var temp = resultstr.replace(regex,' ');
				resultstr = temp.replace(/\s{2,}/gi,' ');
				}

				_this.callback(resultstr);
				_this.reset();
				
				event.preventDefault();
				return false;
				}, true);
				//this.wireUpEvents();
			},
		
			valueOf: function(name) {
				return this.frm.elements.namedItem(name).value;
			},

			// Currently un-used, but kept around in case I want to
			// re-enable live preview.
			wireUpEvents: function() {
			var output_div = this.getByClass(this.editor, "output")[0];
			var p_block_div = this.getByClass(this.editor, "previewblock")[0];

			this.sample_field = this.getByClass(output_div, "samplecode")[0];
			this.compact_field = this.getByClass(output_div, "compactcode")[0];
			this.preview_div = this.getByClass(p_block_div, "preview")[0];
			
			// Build some closure functions for GUI calendar eventsevents.
			var _this = this;
			var doReset = function(event) {
				_this.reset();
				event.preventDefault(); 
			}; 
			var doUpdateContent = function(event) {
				_this.updateContent();
				event.preventDefault(); 
			}; 
			var doUpdate = function(event) {
				_this.update();
				event.preventDefault(); 
			}; 
			var doUpdateEndTime = function(event) {
				_this.update_endtime();            
				event.preventDefault(); 
			};

			this.frm.addEventListener('reset', doReset, true); 
			this.frm.addEventListener('submit', doUpdateContent, true);

			var inputs = this.editor.getElementsByTagName('input');
			for (var i = 0; i < inputs.length; i++) { 
				inputs[i].addEventListener('click', doUpdateContent, true); 
				inputs[i].addEventListener('keyup', doUpdateContent, true);
			}

            var selects = this.editor.getElementsByTagName('select');

			for (var i = 0; i < selects.length; i++) { 
				selects[i].addEventListener('click', doUpdateContent, true); 
				selects[i].addEventListener('keyup', doUpdateContent, true);
			} 
            this.frm.elements.namedItem('description').addEventListener( 
				'keyup', doUpdateContent, true); 
			this.frm.elements.namedItem('startYear').addEventListener( 
				'change', doUpdate, true); 
			this.frm.elements.namedItem('startMonth').addEventListener( 
                'change', doUpdate, true); 
            this.frm.elements.namedItem('startDay').addEventListener( 
                'change', doUpdate, true); 
            this.frm.elements.namedItem('endHour').addEventListener( 
                'change', doUpdateEndTime, true); 
            this.frm.elements.namedItem('endMinute').addEventListener( 
                'change', doUpdateEndTime, true);
            this.reset(); 
        },

		getByClass: function(parent, cls) { 
            var i, c; 
			var cs = parent.childNodes; 
			var rv = []; 
			for (i=0; i<cs.length; i++)
                if (cs[i].className && cs[i].className == cls) 
                    rv[rv.length] = cs[i]; 
		 	return rv; 
	    },

		getSelectedValue: function(name) { 
		    var elmSelect = this.valueOf(name); 
			return elmSelect.options[elmSelect.selectedIndex].text;
        },

		buildContent: function() { 
		    // Enforce proper values for the start/end times. 
			if (this.valueOf('startHour') > 23) {
                this.frm.elements.namedItem('startHour').value = 23; 
    	    } 
     		if (this.valueOf('startHour') < 0) {
                this.frm.elements.namedItem('startHour').value = 0; 
            } 
		    if (this.valueOf('endHour') > 23) {
                this.frm.elements.namedItem('endHour').value = 23; 
            } 
		    if (this.valueOf('startHour') < 0) {
                this.frm.elements.namedItem('startHour').value = 0; 
            } 
            if (this.valueOf('startMinute') > 59) {
                this.frm.elements.namedItem('startMinute').value=59; 
			} 
            if (this.valueOf('startHour') < 0) {
                this.frm.elements.namedItem('startHour').value = 0; 
            } 
            if (this.valueOf('endMinute') > 59 ) {
                this.frm.elements.namedItem('endMinute').value = 59; 
            } 
		    if (this.valueOf('startHour') < 0) {
                this.frm.elements.namedItem('startHour').value = 0; 
            }

		    /* get values of text fields */
   		    var summary        = this.valueOf('summary');
		    var url            = this.valueOf('url');

		    var startYear      = this.valueOf('startYear');
		    var startMonth     = this.valueOf('startMonth');
		    var startDay       = this.valueOf('startDay');
		    var startHour      = this.valueOf('startHour'); 
		    var startMinute    = this.valueOf('startMinute');
  		    var endHour        = this.valueOf('endHour'); 
		    var endMinute      = this.valueOf('endMinute');

		    var endYear        = this.valueOf('endYear'); 
		    var endMonth       = this.valueOf('endMonth'); 
		    var endDay         = this.valueOf('endDay');

		    var startMonthText = this.getSelectedValue('startMonth'); 
		    var startDayText   = this.getSelectedValue('startDay'); 
		    var endDayText     = this.getSelectedValue('endDay'); 
		    var endMonthText   = this.getSelectedValue('endMonth');

		    var timezone       = this.valueOf('timezone'); 
		    var description    = this.valueOf('description');

		    if(!timezone) timezone = '';

		    if(timezone > 0) timezone = '+' + timezone;

            if (this.late_night()) { var late = true; }

		    if (startMinute) startMinute = this.pad(startMinute);
		    if (startHour)	 startHour	 = this.pad(startHour);
 		    if (endMinute)   endMinute   = this.pad(endMinute);	
		    if (endHour)     endHour     = this.pad(endHour);
		
            var dtstart = startYear + startMonth + startDay;
 
            if (startHour) { 
                if(!startMinute) startMinute= '00'; 
				dtstart += 'T' + startHour + startMinute + timezone;
			}

			var dtend = endYear + endMonth + endDay;

			if (endHour) { 
				if(endHour.length < 2) {
				endHour = '0' + endHour; 
				} 
				if (!endMinute) endMinute = '00'; 
				dtend += 'T' + endHour + endMinute + timezone;
			}
			var startOut = startMonthText + ' ' + startDayText;

			if (startYear != endYear) { 
				startOut += ', ' + startYear; 
			}

			var endOut = ''; 
			if(!late) {
            if(startMonth != endMonth || startYear != endYear) {
                endOut += endMonthText + ' ';
			}

			if(!(startMonth == endMonth && startYear == endYear &&
				startDay == endDay)) {
                endOut += endDay;
			} else {
				startOut += ', ' + startYear;
			} 
		} 
		if(startHour && startMinute) {
			startOut += ' - ' + startHour + ':' + startMinute; 
			if (endOut) { 
				var collapse = true; 
			} 
		}

		if (endHour && endMinute) { 
			if (collapse) {
				endOut += ' - '
			}
			endOut += endHour + ':' + endMinute
		}

		if(!(startMonth == endMonth && startYear == endYear && 
				startDay == endDay)) { 
			endOut += ', ' + endYear; 
		}

		var location = this.valueOf('location');

		/* set results field */
		var resultstr = '<div class="vevent">\n';
		if (url) {
			resultstr += ' <a class="url" href="' + url + '">\n'; 
		} 
		resultstr += ' <abbr class="dtstart" title="' + dtstart +
			'">\n ' + startOut + '\n <\/abbr> - \n';

		if (!((startYear+startMonth+startDay==endYear+endMonth+endDay) &&
				!endHour)) { 
			resultstr += ' <abbr class="dtend" title="' + dtend + '">'; 
			if (endOut) resultstr += '\n ' + endOut + '\n '; 
			resultstr += '<\/abbr>\n';
		}
		if (endHour && endMinute) resultstr += ' - ';
		resultstr += ' <span class="summary">\n ' +
			this.escape_output(summary) + '\n </span> ';
		if (location) resultstr += '- at\n <span class="location">\n '+ 
            this.escape_output(location) + '\n ' + 
			'<\/span>';
		if (url) {
            resultstr += '\n </a>\n';
        }

		if(description) resultstr+='\n <div class="description">\n '+ 
			this.escape_output(description) + '\n </div>\n';

		resultstr += '\n<\/div>';

		return resultstr; 
	},

	updateContent: function() {        
		var resultstr = this.buildContent();
		
		this.sample_field.value = resultstr;
		this.preview_div.innerHTML = resultstr;

		var regex = /\n/gi;
		var temp = resultstr.replace(regex,' ');
		temp = temp.replace(/\s{2,}/gi,' ');
		this.compact_field.value = temp;

	},
	
	update: function() { 
		var startYear	= this.valueOf('startYear'); 
		var startMonth	= this.valueOf('startMonth'); 
		var startDay	= this.valueOf('startDay');

		var endYear		= this.valueOf('endYear');
		var endMonth	= this.valueOf('endMonth');
		var endDay		= this.valueOf('endDay');

		this.frm.elements.namedItem('endYear').value = 
		    this.valueOf('startYear'); 
		this.frm.elements.namedItem('endMonth').value = 
			this.valueOf('startMonth'); 
		this.frm.elements.namedItem('endDay').value = 
			this.valueOf('startDay'); 
	},

	update_endtime: function() { 
		var startYear	= this.valueOf('startYear'); 
		var startMonth	= this.valueOf('startMonth'); 
		var startDay	= this.valueOf('startDay'); 
		var endYear		= this.valueOf('endYear'); 
		var endMonth	= this.valueOf('endMonth'); 
		var endDay		= this.valueOf('endDay'); 
		var endHour		= this.valueOf('endHour'); 
		var endMinute	= this.valueOf('endMinute'); 
		var startHour	= this.valueOf('startHour'); 
		var startMinute	= this.valueOf('startMinute');

		if (endHour && endMinute && startHour && startMinute &&
				startYear == endYear && startMonth == endMonth &&
				startDay == endDay) {
			var startTime = startHour + startMinute;
			var endTime = endHour + endMinute;

			if(startTime.length == 3) startTime = '0' + startTime;
			if (endTime.length == 3) endTime = '0' + endTime;
				
			if(endTime < startTime){
			    this.increment_end_date();
            }
        }
			
	},

	increment_end_date: function() { 
        var endYear = this.valueOf('endYear'); 
		var endMonth = this.valueOf('endMonth'); 
		var endDay = this.valueOf('endDay');
		
		var d = new Date(endYear, parseInt(endMonth) - 1, parseInt(endDay));

		d.setDate(++endDay);

		this.frm.elements.namedItem('endYear').value =            
            d.getFullYear(); 
		this.frm.elements.namedItem('endMonth').selectedIndex =            
			d.getMonth(); 
		this.frm.elements.namedItem('endDay').selectedIndex =            
			d.getDate() - 1; 
	},

	late_night: function() { 
		//convert to date objects 
		if(parseInt(this.valueOf('endHour')) < 6) {

			var endDate = new Date(this.valueOf('endYear'), 
				this.frm.elements.namedItem('endMonth').selectedIndex, 
				parseInt(this.valueOf('endDay')));
				
			var startDate = new Date(this.valueOf('startYear'), 
				this.frm.elements.namedItem('startMonth').selectedIndex, 
				parseInt(this.valueOf('startDay')));
			//increment and test

			startDate.setDate(startDate.getDate() + 1);

			if(startDate.getYear() == endDate.getYear() &&                    
				startDate.getMonth() == endDate.getMonth() &&              
				startDate.getDay() == endDate.getDay()) {
				return true;
				}
			}

			return false;
		},

		escape_output: function(input){
			// this is not the most robust solution,
			// but it should cover most cases
			var amp = /\s&\s/gi;
			var lt = /\s\<\s/gi;
			var gt = /\s>\s/gi;
			
			var temp = input.replace(amp,' &amp; ');
			temp = temp.replace(lt,' &lt; ');
			var output = temp.replace(gt,' &gt; ');
			return output;
		},
			
		reset: function() {        
			var d = new Date();        
			this.frm.elements.namedItem('startYear').value = d.getFullYear(); 
			this.frm.elements.namedItem('startMonth').selectedIndex = d.
		getMonth();        
			this.frm.elements.namedItem('startDay').value = d.getDate();
			
			this.frm.elements.namedItem('endYear').value = d.getFullYear(); 
			this.frm.elements.namedItem('endMonth').selectedIndex = d.getMonth( 
		);        
			this.frm.elements.namedItem('endDay').value = d.getDate();
			
			var timezone = d.getTimezoneOffset();

			timezone = -timezone / 60;
			timezone = timezone + "00";
			if(timezone.length == 4)
				timezone = timezone.charAt(0) + "0" + timezone.substring(1);

			if (parseInt(timezone) > 0) {
				timezone = "+" + timezone;
			}

			this.frm.elements.namedItem('timezone').value = timezone;
			this.updateContent();
		},

		pad: function(input) {
			if (input.length < 2) input = '0' + input.toString();
			return input;
		},
		GLOBAL_CSS: '\ 
			.hCalEditor .inputs { \
				float:left; margin-right:2em \
			} \
			.hCalEditor label { \
				float:left; \
				clear:left; \
				text-align:right; \
				width:5em; \
				padding-right:1em; \
				font-weight:bold; \
				line-height:1.9em \
			} \ 
			.hCalEditor .field \ 
				{ margin-bottom:.7em; font-size:smaller } \ 
			.hCalEditor .field input \ 
				{ width: 16em; line-height:2em } \ 
			.hCalEditor .submit \ 
				{ margin:1em 0 1em 7em } \ 
			.hCalEditor .submit button, .hCalEditor .submit input \ 
				{ margin-left:1em } \ 
			.hCalEditor form, .hCalEditor fieldset \ 
				{ margin:0 } \ 
			.hCalEditor h2 \ 
				{ margin:.3em 0 .1em 0; font-size:1em } \ 
			.hCalEditor .output \ 
				{ float:left } \ 
			.hCalEditor .previewblock \ 
				{clear:left} \
			.hCalEditor .preview { \ 
				padding:.5em; background:#ccc; \ 
				border:1px solid black; margin-right:2em \
			} \
			.hCalEditor .field .startHour \
				{ width: 41px; } \
			.hCalEditor .field .summary, \
			.hCalEditor .field .location, \
			.hCalEditor .field .url \
				{ width:21em } \
			.hCalEditor .field .startHour, \
			.hCalEditor .field .startMinute, \
			.hCalEditor .field .endHour, \
			.hCalEditor .field .endMinute \
				{width:2em} \ 
			.hCalEditor .field .timezone \ 
				{width:10em} \ 
		',

		EDITOR_HTML: '\ 
			<div id="\0editor_id\f" class="hCalEditor"> \ 
			<div class="inputs"> \      
			<form action="" onreset="doreset();"> \ 
			<fieldset> \ 
			<legend><a href="http://microformats.org/wiki/calendar eventshcalendar"\
				>hCalendar</a>-o-matic</legend> \
			<!-- url, summary, dtstart, dtend, location --> \ 
			<div class="field"> \ 
			<label for="summary">summary</label> \ 
			<input type="text" class="summary" name="summary" \
				value="event title" /> \ 
			</div> \ 
			<div class="field"> \ 
			<label for="location">location</label> \ 
			<input type="text" class="location" name="location" /> \ 
			</div> \ 
			<div class="field"> \ 
			<label for="url">url</label> \ 
			<input type="text" class="url" name="url" /> \ 
			</div> \ 
			<div class="field"> \ 
			<label for="startMonth">start</label> \ 
			<select class="startMonth" name="startMonth" > \ 
			<option value="01">January</option> \ 
			<option value="02">February</option> \ 
			<option value="03">March</option> \ 
			<option value="04">April</option> \ 
			<option value="05">May</option> \ 
			<option value="06">June</option> \ 
			<option value="07">July</option> \ 
			<option value="08">August</option> \ 
			<option value="09">September</option> \ 
			<option value="10">October</option> \ 
			<option value="11">November</option> \ 
			<option value="12">December</option> \ 
			</select> \ 
			<select class="startDay" name="startDay" > \ 
			<option value="01">1</option> <option value="02">2</option> \ 
			<option value="03">3</option> <option value="04">4</option> \ 
			<option value="05">5</option> <option value="06">6</option> \ 
			<option value="07">7</option> <option value="08">8</option> \ 
			<option value="09">9</option> <option value="10">10</option> \ 
			<option value="11">11</option><option value="12">12</option> \ 
			<option value="13">13</option><option value="14">14</option> \ 
			<option value="15">15</option><option value="16">16</option> \ 
			<option value="17">17</option><option value="18">18</option> \ 
			<option value="19">19</option><option value="20">20</option> \ 
			<option value="21">21</option><option value="22">22</option> \ 
			<option value="23">23</option><option value="24">24</option> \ 
			<option value="25">25</option><option value="26">26</option> \ 
			<option value="27">27</option><option value="28">28</option> \ 
			<option value="29">29</option><option value="30">30</option> \ 
			<option value="31">31</option> \ 
			</select> \ 
			<select class="startYear" name="startYear" > \ 
			<option value="2004">2004</option> \ 
			<option value="2005">2005</option> \
			<option value="2006">2006</option> \
			<option value="2007">2007</option> \
			<option value="2008">2008</option> \
			</select> \
			<input type="text" class="startHour" class="startHour" \
				name="startHour" maxlength="2" /> : \ 
			<input type="text" class="startMinute" name="startMinute" \
				maxlength="2" /> \ 
			</div> \ 
			<div class="field"> \ 
			<label for="endMonth">end</label> \ 
			<select class="endMonth" name="endMonth" > \ 
			<option value="01">January</option> \ 
			<option value="02">February</option> \ 
			<option value="03">March</option> \ 
			<option value="04">April</option> \ 
			<option value="05">May</option> \ 
			<option value="06">June</option> \ 
			<option value="07">July</option> \ 
			<option value="08">August</option> \ 
			<option value="09">September</option> \ 
			<option value="10">October</option> \ 
			<option value="11">November</option> \ 
			<option value="12">December</option> \ 
			</select> \ 
			<select class="endDay" name="endDay" > \ 
			<option value="01">1</option> <option value="02">2</option> \ 
			<option value="03">3</option> <option value="04">4</option> \ 
			<option value="05">5</option> <option value="06">6</option> \ 
			<option value="07">7</option> <option value="08">8</option> \ 
			<option value="09">9</option> <option value="10">10</option> \ 
			<option value="11">11</option><option value="12">12</option> \ 
			<option value="13">13</option><option value="14">14</option> \ 
			<option value="15">15</option><option value="16">16</option> \ 
			<option value="17">17</option><option value="18">18</option> \ 
			<option value="19">19</option><option value="20">20</option> \ 
			<option value="21">21</option><option value="22">22</option> \ 
			<option value="23">23</option><option value="24">24</option> \ 
			<option value="25">25</option><option value="26">26</option> \ 
			<option value="27">27</option><option value="28">28</option> \ 
			<option value="29">29</option><option value="30">30</option> \ 
			<option value="31">31</option> \ 
			</select> \ 
			<select class="endYear" name="endYear" > \ 
			<option value="2004">2004</option> \ 
			<option value="2005">2005</option> \ 
			<option value="2006">2006</option> \ 
			<option value="2007">2007</option> \ 
			<option value="2008">2008</option> \ 
			</select> \
			<input type="text" class="endHour" name="endHour" \ 
				maxlength="2" /> : \ 
			<input type="text" class="endMinute" name="endMinute" maxlength="2" />
	\ 
			</div> \ 
			<div class="field"> \ 
			<label for="timezone">TZ</label> \ 
			<select class="timezone" name="timezone"> \ 
			<option value="">none</option> \ 
			<option value="-1200">-12 (IDLW)</option> \ 
			<option value="-1100">-11 (NT)</option> \ 
			<option value="-1000">-10 (HST)</option> \ 
			<option value="-900">-9 (AKST)</option> \ 
			<option value="-0800">-8 (PST/AKDT)</option> \ 
			<option value="-0700">-7 (MST/PDT)</option> \ 
			<option value="-0600">-6 (CST/MDT)</option> \ 
			<option value="-0500">-5 (EST/CDT)</option> \ 
			<option value="-0400">-4 (AST/EDT)</option> \ 
			<option value="-0345">-3:45</option> \ 
			<option value="-0330">-3:30</option> \ 
			<option value="-0300">-3 (ADT)</option> \ 
			<option value="-0200">-2 (AT)</option> \ 
			<option value="-0100">-1 (WAT)</option> \ 
			<option value="Z">+0 (GMT/UTC)</option> \ 
			<option value="+0100">+1 (CET/BST/IST/WEST)</option> \ 
			<option value="+0200">+2 (EET/CEST)</option> \ 
			<option value="+0300">+3 (MSK/EEST)</option> \ 
			<option value="+0330">+3:30 (Iran)</option> \ 
			<option value="+0400">+4 (ZP4/MSD)</option> \ 
			<option value="+0430">+4:30 (Afghanistan)</option> \ 
			<option value="+0500">+5 (ZP5)</option> \ 
			<option value="+0530">+5:30 (India)</option> \ 
			<option value="+0600">+6 (ZP6)</option> \ 
			<option value="+0630">+6:30 (Burma)</option> \ 
			<option value="+0700">+7 (WAST)</option> \ 
			<option value="+0800">+8 (WST)</option> \ 
			<option value="+0900">+9 (JST)</option> \ 
			<option value="+0930">+9:30 (Central Australia)</option> \ 
			<option value="+1000">+10 (AEST)</option> \ 
			<option value="+1100">+11 (AEST(summer))</option> \ 
			<option value="+1200">+12 (NZST/IDLE)</option> \ 
			</select> \ 
			hour(s) from <abbr title="Greenwich Mean time">GMT</abbr> \ 
			</div> \ 
			<div class="field"> \ 
			<label for="description">description</label> \ 
			<textarea class="description" name="description" cols="33" \
				rows="5"></textarea> \
			</div> \
			<div class="submit"> \
			<input type="checkbox" name="compact" value="compact" /> \
				Compact? \ 
			<input type="button" name="build" value="Build and Insert" /> \
			<input type="reset" class="reset" name="reset" /> \
			</div> \
			</fieldset> \
			</form> \
			</div> \
			</div> \
		', 
	};

	var MagicMF = {

		init: function() {
			var textareas, textarea;

			DBUtils.addGlobalStyle(MagicMF.GLOBAL_CSS); 
			DBUtils.addGlobalStyle(calendar eventsHCalendarCreator.prototype.GLOBAL_CSS);
			
			textareas = document.getElementsByTagName('textarea'); 
			if (!textareas.length) { return; }

			for	(var i = 0; i < textareas.length; i++) {
				textarea = textareas[i];

			button = MagicMF.createButton 
				(i, textarea, MagicMF.handleMagicButton, 
				"Build event here", 0, 0, '');

			textarea.parentNode.insertBefore(button, textarea);
			textarea.parentNode.insertBefore(
				document.createElement('br'), textarea);
		}
	},

	insertText: function(ele, ins_val) { 
		// Find the start/end of the selection, and the total length 
		var start = ele.selectionStart; 
		var end = ele.selectionEnd; 
		var length = ele.textLength; 
		var val = ele.value;

		// If nothing selected, jump to the end.
		if (end == 1 || end == 2) end = length;

		// Replace the selection with the incoming value. 
		ele.value = val.substring(0, start) + ins_val + 
				val.substr(end, length);

		// Place the cursor at the end of the inserted text & refocus the
		// textarea
		//e.selectionStart = start;
		ele.selectionStart = start + ins_val.length;
		ele.selectionEnd = start + ins_val.length;
		ele.focus(); 
	},

	handleMagicButton: function(event, textarea) { 
		var link, textarea, s; 
		link      = event.currentTarget; 
		button_id = link.id;

		var panel_id = button_id + "_panel";
		var panel	= document.getElementById(panel_id);
		if (!panel)
			panel = MagicMF.createPanel(button_id, panel_id, textarea);

		panel._start = textarea.selectionStart;
		panel._end	 = textarea.selectionEnd;

		DBUtils.toggle(panel_id); 
	},

	createPanel: function(button_id, panel_id, textarea) {

		var taX = DBUtils.findElementX(textarea);
		var taY = DBUtils.findElementY(textarea);
		var taW = textarea.offsetWidth;
		var taH = textarea.offsetHeight;

		var txt_id	  = panel_id + "_txt";
		var editor_id = panel_id + "_editor";

		var css = DBUtils.format(MagicMF.PANEL_CSS, {
			id:		panel_id,
			txt_id: txt_id,
			left:	taX,
			top:	taY,
			width:	taW - 24,
			height: taH - 24,
		});
		DBUtils.addGlobalStyle(css);

		var _this = this;
		var panel = document.createElement("div");
		var cb    = function(val) {
            _this.panelCallback(panel, val, textarea);
		}
		var editor = new calendar eventsHCalendarCreator(editor_id, cb);
		var editor_html = DBUtils.format(editor.EDITOR_HTML, {
			editor_id: editor_id
		});

		panel.id        = panel_id;
		panel.innerHTML = DBUtils.format(MagicMF.PANEL_HTML, {
            id:           panel_id,
			txt_id:       txt_id,
			editor_html:  editor_html
		});
		document.body.appendChild(panel);

		editor.init();
	
		return panel; 
	},

	panelCallback: function(panel, val, textarea) { 
		MagicMF.insertText(textarea, val); 
		DBUtils.hide(panel.id);
	},

	createButton: function(sub_id, target, func, title, width, height, 
        src) { 
		var img, button;

		img = document.createTextNode("[ hCal ]");

		button        = document.createElement('a');
		button.id     = 'mf_'+sub_id;	
		button.title  = title;
		button.href = '#';
		button.addEventListener('click', function(event) { 
			func(event, target);            
			event.preventDefault();
		}, true);
		var spot = document.createElement('a');
		spot.name = button.id;

		button.appendChild(spot);
		button.appendChild(img);
		return button;
	},

	GLOBAL_CSS: '\ 
		.mf_editor_close { \ 
			float: right \ 
		} \ 
	',

	PANEL_CSS: ' \
		#\0id\f { \
			position: absolute; \
			display:  none; \
			overflow: auto; \
			margin:   2px; \
			padding:  10px; \
			left:     \0left\fpx; \
			top:      \0top\fpx; \
			width:    \0width\fpx; \
			height:	  \0height\fpx; \
			color:    #000000; \
			z-index:  999; \
			background-color: #eeeeee; \
		} \
	',

	PANEL_HTML: '\
      <a class="mf_editor_close" \
			onClick="DBUtils.hide(\'\0id\f\')">[ X ]</a> \
		\0editor_html\f \
	',
};
var DBUtils = {
	/*
		format(template string, template map): 
			Populates a template string using map lookups via 
			named slots delimited by \0 and \f.
	*/
	format: function(tmpl, tmpl_map) {
		var parts = tmpl.split(/(\0.*?\f)/);
		var i, p, m, out="";
		for (i=0; i<parts.length; i++) {
			p = parts[i]; 
			m = p.match(/^\0(.*?)\f$/); 
			out += (!m) ? p : tmpl_map[m[1]];
		}
		return out;
	},

	hide: function(id) {
		var that = document.getElementById(id);
		if (that) that.style.display = 'none';
	},

	show: function(id) {
		var that = document.getElementById(id);
		if (that) that.style.display = 'block';
	},

	toggle: function(id) {
		var that = document.getElementById(id);
		if (!that) return;
		that.style.display =
			(that.style.display == 'block') ? 'none' : 'block'; 
	},

	// from http://www.quirksmode.org/js/findpos.html
	findElementX: function(obj) {
		var curleft = 0;
		if (obj.offsetParent) {
				while (obj.offsetParent) {
				curleft += obj.offsetLeft;
				obj = obj.offsetParent;
				}
			}
			else if (obj.x)
				curleft += obj.x;		
			return curleft;
		},

		findElementY: function(obj) {
			var curtop = 0;
			if (obj.offsetParent) {
				while (obj.offsetParent) {
				curtop += obj.offsetTop;
				obj = obj.offsetParent;
				}
			}
			else if (obj.y)
				curtop += obj.y;
			return curtop;
		},

		addGlobalStyle: function(css) {
			var head, style;
			head = document.getElementsByTagName('head')[0];
			if (!head) { return; }
			style = document.createElement('style');
			style.type = 'text/css';
			style.innerHTML = css;
			head.appendChild(style);
		},
	};

	// Now that everything's defined, fire it up.
	MagicMF.init();

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.htmlcodetutorial.com/forms/_TEXTAREA.html. The script adds the hCal link above the <textarea>.

Tip

Since this hack adds its interface to text areas, you can combine it with "Resize Text Input Fields with the Keyboard" [Hack #34] to increase the size of the <textarea> field.

Click the hCal link to display the HCalendar interface, as shown in Figure 4-14.

Figure 4-14. HCalendar interface

HCalendar interface

Fill in the event information, click the Build and Insert button, and the script will convert the information into HCalendar markup, as shown in Figure 4-15.

Figure 4-15. HCalendar event information

HCalendar event information

Is this hack useful? Not yet, because few sites support the emerging HCalendar standard. (Two that do are http://upcoming.org and http://evdb.com.) But it serves as a powerful demonstration of how far Greasemonkey can go to change the user experience—in this case, from a single <textarea> to an entire graphical interface optimized for a specific microformat.

Personal tools