We do a lot of development for internal tools at CNET, and this is a great place to really bring clientside technologies into play. Javascript can really help speed up data entry; it can even make it kinda fun if you let it...
This small script just lets you put things on the user's OS clipboard. It depends on a flash plugin that you can find here and borrows completely from this previous work.
Clipboard.copy('this is now on your clipboard'); //execute this, then paste somewhere

$('clipboardExample').addEvent('click', function() { Clipboard.copyFromElement('clipboardExample'); }); //execute this, then select some text in the input above //it will then be on your clipboard

Confirmer is a class that lets you easily notify a user that something has changed. Typically, this occurs when you have a form input that updates as the user interacts with it. There my not even be a "save" button. Or perhaps it auto saves a draft like gmail or something.
The Confirmer class lets you tell the user this without much hassle, though it's very configurable.
There are two basic ways to use the class. One is to fade in and out a message already in the dom. Here's a quick example.
The HTML:
<input id="confirmerInDom" type="text"> <span id="confirmerInDomMsg" style="visibility: hidden; color: red; font-weight: bold">you updated the field!</span> <script> var conf = new Confirmer({ msg: 'confirmerInDomMsg', reposition: false }); $('confirmerInDom').addEvent('change', conf.prompt.bind(conf)); </script>
Try it out; just type something and then tab out of the field:
you updated the field!
This simple example doesn't really capture what the class can do though. The other way to use it is to position the message "over" the content relative to an element on the page. Here are a few examples:
var conf2 = new Confirmer({ positionOptions: { relativeTo: "confirmerNearInput", position: "bottomLeft", offset: {x: 0, y: 10} } }); $('confirmerNearInput').addEvent('change', conf2.prompt.bind(conf2));

Execute the example and then type something in the block.
The default location for the message is the upper right corner of the window:
/*this example uses the default message and the default location: the upper right of the window*/ new Confirmer().prompt();

Finally, you can prompt different messages using the same prompter:
var conf2 = new Confirmer({ positionOptions: { relativeTo: "confirmerThatChanges", position: "bottomLeft", offset: {x: 0, y: 10} } }); $('confirmerThatChanges').addEvent('change', function(){ conf2.prompt({msg: 'you entered ' + this.value}); });

Nothing fancy here. Just a Mootools version of a date picker. This has dependencies on our StickyWin class and our Date extension.
new DatePicker('dateExample', { additionalShowLinks:['dateExampleImg'], format: '%a, %b %d %Y'//Mon, Jan 01 2007 })

new DatePicker('dateExample2', { showOnInputFocus: false })

Here we have an example where we aren't showing the calendar at all, but we are using the class to help the user enter a valid date. Enter in a date using several different formats and you'll see it correct the entry when the input looses focus.
Finally, here I have an example that combines DatePicker with FormValidator. Note that this example doesn't show the calendar on focus, but there's a calendar image. This is the usage that I think should be the default. It's not because I don't assume you're passing in a value for additionalShowLinks, but if you do, it's probably best to turn of the focus on show option.
new DatePicker('dateExample3', { showOnInputFocus: false, additionalShowLinks:['dateExampleImg3'] }); new FormValidator('datePickerValidated');

DatePicker has a lot of options, so be sure to dig into the docs.
When you include this script in your environment you get some extra functionality with DatePicker: time entry and range selection.
new DatePicker('dateTimeExample', { additionalShowLinks:['dateTimeExampleImg'], time: true, format: '%x %X' //12/31/1999 12:01AM })

new DatePicker($$('#dateRangeExampleStart, #dateRangeExampleEnd'), { additionalShowLinks:['dateRangeExampleStart', 'dateRangeExampleEnd'], range: true, format: '%x' //12/31/1999 })

new DatePicker($$('#dateTimeRangeExampleStart, #dateTimeRangeExampleEnd'), { additionalShowLinks:['dateTimeRangeExampleStart', 'dateTimeRangeExampleEnd'], range: true, time: true, format: '%x %X' //12/31/1999 12:45PM })

This form validator class is inspired by Really easy field validation with prototype by Andrew Tetlaw. My version is actually a little larger, but it has more functionality (and it's based on Mootools).
The InputValidator class creates a single object that will validate a form input for a specific type of input. For instance, you might have an InputValidator for required fields that just makes sure the user has input something, while another InputValidator might check for a date format.
It's important to note that these are all javascript which means that they are easily circumvented. For security's sake, you should still validate the input on the receiving end (your php or java servlet or whatever).
Here's what an InputValidator looks like:
var isEmpty = new InputValidator('required', { errorMsg: 'This field is required.', test: function(field){ return ((field.getValue() == null) || (field.getValue().length == 0)); } }); if(isEmpty.test($("firstName"))) /*true if empty*/ simpleErrorPopup('input error', isEmpty.getError($("firstName"))); /*alerts "This field is required."*/

If you execute the code above with the input empty, you'll get an error message.
The FormValidator class will validate an entire form using a collection of input validators. Each input in the form is given a classname that corresponds to an input validator, and then as the user interacts with the form the inputs are validated.
The library comes with numerous validators already defined, but adding new ones is easy enough. For instance, to add the validator in the example above (though it's already defined by default, but just to illustrate the process...):
FormValidator.add('IsEmpty', { errorMsg: 'This field is required.', test: function(element) { return ((element.getValue() == null) || (element.getValue().length == 0)); } });
Now if you give any input the class "IsEmpty" the FormValidator can validate that.
You can also add these in bulk with an array of validators, but you can see this in action in the script and I won't illustrate it here.
Enough with the details, here is form validator in action. Here's our sample form:
new FormValidator('formExample');

Ok, execute the code example above, then start filling out the form. Skip through some fields and enter some bad data in others (like a non-number value for age).
Each Validator can also be used to generate warnings. Warnings still show error messages, but do not prevent the form from being submitted. Warnings can be applied in two ways.
Example:
//I've already defined validator2 for scope reasons validator2 = new FormValidator('formExample2');

In this example I've set up the first field to warn you if you exceed 20 characters or input less than 10, but it won't fail validation and you can submit the form as long as you have one character in the field.
<input type="text" name="userName" id="userName2" class="required warn-minLength warn-maxLength" validatorProps="{minLength:10, maxLength:20}">
As you can see, I've pre-pended "warn-" to the same class names I would have given it previously. By adding "warn-" to the classname for a validator, you tell the class that these things should still provide hints, but the form is ok to submit if they don't pass. Note that required does not have a "warn-" prefix. This means that the field can't be empty. In this way you can provide validators that give feedback but allow for a more flexible input.
You may find that at some point the context of your form has changed and you want to turn off validation for certain fields. For instance, maybe you want to force users who are over 13 to give you their zip code, but for kids you are going to hide this input. You need to stop monitoring that field because the context changed.
validator2.enforceField('birthDate2'); //$('birthDate2')'s validator is now required to pass //(in this case, that the date is a valid format)

validator2.ignoreField('birthDate2'); //$('birthDate2') now will take any string without complaint

You can also make all the validators switch to warning mode (so the field can be submitted even if they all fail):
validator2.ignoreField('homePage2', true); //$('homePage2') will complain if you put in an //invalid url, but you can still submit the form.

Note that the first input above enforces not only that the input is required, but also that it is a certain length (between 10 and 99 chars). Validators can have configurations for individual fields. This is done with an html property called "validatorProps" which has an object with key/value definitions. Here's what that first input above looks like:
<input type="text" name="userName" id="userName" class="minLength maxLength" validatorProps="{minLength:10, maxLength:20}">
You can see that the class for minLength and maxLength are applied, but the validator needs to know what these numbers are. The object in the "validatorProps" ({minLength:10, maxLength:20}) is passed along to each validator that gets executed, so the validator for minLength looks like this:
FormValidator.add('minLength', { errorMsg: function(element, props){ if($type(props.minLength)) return 'Please enter at least ' + props.minLength + ' characters (you entered ' + element.value.length + ' characters).'; else return ''; }, test: function(element, props) { if($type(props.minLength)) return (element.value.length >= $pick(props.minLength, 0)); else return true; } });
You can see that the InputValidator class that's created with these options (FormValidator.add creates an instance of InputValidator) passes along both the input element and the props object for that element.
You can use FormValidator.add to add validators to every instance of FormValidator, but you can also create validators for a specific instance of the Class. The .add method and the .addAllThese method are properties of every instance of FormValidator as well as the FormValidator object itself. Adding validators to an instance of FormValidator will make those validators apply only to that instance, while adding them to the Class will make them available to all instances.
Examples:
//add a validator for ALL instances FormValidator.add('isEmpty', { errorMsg: 'This field is required', test: function(element){ if(element.value.length ==0) return false; else return true; } }); //this validator is only available to this single instance var myFormValidatorInstance = new FormValidator('myform'); myFormValidatorInstance.add('doesNotContainTheLetterQ', { errorMsg: 'This field cannot contain the letter Q!', test: function(element){ return !element.getValue().test('q','i'); } }); //Extend FormValidator, add a global validator for all instances of that version var NewFormValidator = FormValidator.extend({ //...some code }); NewFormValidator.add('doesNotContainTheLetterZ', { errorMsg: 'This field cannot contain the letter Z!', test: function(element){ return !element.getValue().test('z','i'); } });
FormValidator inserts error messages right after inputs when it generates an error. These slide into place. These blocks of text have the class "validation-advice" and you can make them look like whatever you like. Additionally, each field that is validated gets the class of either "validation-failed" or "validation-passed", so you can also highlight the input. There are also classes for the warning advice and the field when a warning is present. Here's the style I've applied to the inputs above:
.validation-failed { border: 1px solid #f00; } .validation-passed { border: 1px solid green; } .validation-advice { margin: 2px; padding: 2px; color:#fff; background-color:#f00; } .warning { border: 1px solid #c66; } .warning-advice { margin: 2px; padding: 2px; color:#fff; background-color:#bbb; }
Finally, you can instruct FormValidator to use the titles of inputs for the validation message. The default is not to do this, but if you enable it and an input has a title, when the input fails to validate the title value of the field will be displayed instead of the validator's default error message.
OverText is a class that lets you overlay instructions above an input. You can accomplish a similar effecty by setting the value of the input as the instruction and then resetting the value when the user clicks into the input, but this method has three downsides: the instruction might get submitted by the form, you can't style the instruction text differently, if you're using a clientside form validator like FormValidator.js then the value will fail validation.
So OverText places a new DOM element above the input and hides it and shows it when there clicks in and out of an empty input:
Here's the html I'm using:
div.overTxtDiv {
font-weight: bold;
font-family: arial, helvetica, verdana;
font-size: 12px;
color: #999;
}
</style>
<input id="overTextTest" alt="Enter some text here" style="width: 300px">
new OverText($$('#overTextTest'));

This is a fairly complex application, but it's designed to work with any data source that you want, so I'm including it here.
The nutshell is that this plugin lets users search for data and then select an item and then handle that selection (typically updating the input with the proper value).
The ProductPicker is broken up into three classes:
Perhaps an example would be easier to grok. Here I create a ProductPicker that uses the CNET API to retrieve product ids for items available on CNET.com.
new ProductPicker('PickerExample', [CNETProductPicker]);

Execute the code, then click inside the input. Search for something like "ipod" and then choose a product. The input will get the appropriate id to reference the item.
So ProductPicker handles the UI and whatnot, but in order to be useful we have to be able to describe any data source, configure the input so that the user can search it, handle the data returned by that source, and then interact with the page when the user makes a selection.
Enter Picklets. These classes contain just that information that is unique to a given data source. Each Picklet has a css className associated with it as well as all the functionality described above.
Here's an example Picklet - go ahead an execute this example and add it to the list of available Picklets for the ProductPicker
var CNETProductPicker2 = new Picklet('CNETProductPicker2',{ url: 'http://api.cnet.com/restApi/v1.0/techProductSearch', descriptiveName: 'CNET Product Picker Sortable', callBackKey: 'callback', /*see JsonP options*/ data: { partTag: 'mtvo', iod: 'hlPrice', viewType: 'json', sortDesc: 'true' }, /*static data*/ getQuery: function(data){ /*return Ajax or JsonP*/ return new JsonP(this.options.url, { callBackKey: this.options.callBackKey, data: $merge(this.options.data, data) }); }, inputs: { query: { tagName: 'input', type: 'text', instructions: 'search for: ', tip: 'cnet product search::input a product name and hit <enter> to get results', value: '', style: { width: '100%' } }, orderBy: { tagName: 'select', instructions: 'order by: ', style: { width: '100%' }, value: ['pop9%2Bdesc', 'edRating7'], optionNames: ["most popular", "editor's rating"] }, submit: { tagName: 'input', type: 'submit', style: { cssFloat: 'right' }, instructions:'', value: 'submit' } }, /*form builder*/ previewHtml: function(data){ var html = "<div class='dataId' style='color: #999; font-weight:bold; margin: 0px; padding: 0px;'>id: "+data['@id'] +"</div><div class='dataDetails' style='font-size: 10px;'><img height='45' width='"+data.ImageURL[0]['@width']+"' style='margin-left: 10px' src='"+data.ImageURL[1].$+"'/><br /><b>" + data.Name.$ + "</b>"; if(data.EditorsRating && data.EditorsRating.$) html += "<br/>editors' rating: "+data.EditorsRating.$; html += "<div>"; if(data.LowPrice && data.LowPrice.$) html += "<span class='productPickerPrices'>"+data.LowPrice.$+"</span>"; if(data.HighPrice && data.HighPrice.$ && (data.LowPrice.$ != data.HighPrice.$)) html += " to <span class='productPickerPrices'>"+data.HighPrice.$ +"</span>"; html += "</div></div>"; html += "<div>"; if(data.Offers && data.Offers['@numFound'] > 0) html += "resellers: " + data.Offers['@numFound']; html += "</div>"; return html; }, /*html template for returned json data*/ resultsList: function(results){ if(results.CNETResponse.TechProducts && results.CNETResponse.TechProducts["@numFound"] > 0) return results.CNETResponse.TechProducts.TechProduct; return false; }, listItemName: function(data){ return data.Name.$ }, /*line item name for the selection list*/ listItemValue: function(data){ return data['@id']; }, /*handle the click event; user chooses an item, and this function updates the input (or does something else)*/ updateInput: function(input, data) { input.value = data['@id']; } }); ProductPicker.add(CNETProductPicker2);
Now we have two picklets available on this page: CNETProductPicker and CNETProductPicker2. They are nearly identical, but you'll see what makes them different here:
new ProductPicker('PickerExample2', [CNETProductPicker, CNETProductPicker2]);

Execute this block and click the input. You should now see a picker with a select list that lets you choose which data source (the Picklet) you want to use to search.
Now we have numerous Picklets and let's say we want to add ProductPickers to a form with numerous inputs. Here's our html:
<form id="FormPickersExample"> <input type="text" class="CNETProductPicker"> <input type="text" class="CNETProductPicker2"> <input type="text" class="CNETProductPicker CNETProductPicker2"> </form>
Here's that code rendered:
Now our javascript:
new FormPickers('FormPickersExample');

As you click into each input, note how they get the appropriate Picklets.
Another thing we do a lot in our CMS environment is enter text. We have a simple text editor (see below) that helps users author html (it is not a WYSIWYG editor) and it supports custom commands and tags. Some tags require the user to enter data, for instance, an image tag. The user must supply things like the url to the image and the dimensions. Additionally, our CMS has custom tags that we use that aren't standard html. So what we have here is a shortcut to prompt the user to fill in the gaps in such a tag.
The syntax to create a new one looks like this:
var imgBuilder = new TagMaker.extend({ name: "Image Builder", output: '<img src="%Full Url%" width="%Width%" height="%Height%" alt="%Alt Text%" style="%Alignment%"/>', help: { 'Full Url':'Enter the external URL (http://...) to the image', 'Width':'Enter the width in pixels.', 'Height':'Enter the height in pixels.', 'Alt Text':'Enter the alternate text for the image.', 'Alignment':'Choose how to float the image.' }, example: { 'Full Url':'http://i.i.com.com/cnwk.1d/i/hdft/redball.gif' }, 'class': { 'Full Url':'validate-url required', 'Width':'validate-digits required', 'Height':'validate-digits required', 'Alt Text':'required' }, selectLists: { Alignment: [ { key: 'left', value: 'float: left' }, { key: 'right', value: 'float: right' }, { key: 'none', value: 'float: none', selected: true }, { key: 'center', value: 'margin-left: auto; margin-right: auto;' } ] }, showResult: false });
This just creates the instance though. You still need to attach it to something. For this, you use the method prompt and pass it the element you want it associated with. This means you can have one instance for numerous inputs and just prompt it with each element as you use it.
$$('#input1, #input2').addEvent('click', function(){ imgBuilder.prompt(this); });
Here's a working example:
Included in the library are two default examples: an image tag and an anchor tag: TagMaker.image and TagMaker.anchor. You can see both of these in action in the simple html editor below.
As much as I'd like to implement a wysiwyg editor or use one of the 3rd party ones out there like TinyMCE or FCKeditor, or even the Mootools wysiwyg by Inviz, it doesn't suit my needs right now. This annoys me, but the fact is that wysiwyg editors built in browsers are fraught with peril. Creating one with flash is an option, but I'm not a flash expert.
So what I've created is an html editor that just helps you wrap your content with custom tags. I need to add tag support and that sort of thing like posteditor (or I might just integrate posteditor).
Anyway, basically this works kinda the way my form validator works. You create commands (like "bold" or "underline") and add them to the editor's list of commands. You can add commands to an instance or to the global set. Then you add buttons to your editor that reference the command and you're set.
Execute the code below, then add some text to the area above, select it, and click some of the editor links...
new SimpleEditor($('simpleEditor'), $$('#editToolbar img'));

Here's an example of what it takes to add a command to the editor:
/* Default commands: */ SimpleEditor.addCommands({ /* bold - <b></b> */ bold: { shortcut: 'b', command: function(input){ input.insertAroundCursor({before:'<b>',after:'</b>'}); } }, /* underline - <u></u> */ underline: { shortcut: 'u', command: function(input){ input.insertAroundCursor({before:'<u>',after:'</u>'}); } } });
And then to make these work, you add a link or image or whatever to your editor like so:
<img src="bold.gif" width="20" height="20" alt="Bold (ctrl+b)" title="Bold (ctrl+b)" rel="bold"/> <img src="underline.gif" width="20" height="20" alt="Underline (ctrl+u)" title="Underline (ctrl+u)" rel="underline"/>
In this case I use images and pass those along when I create a new instance of the editor. It uses the rel attribute to map to the command.
cnet-libraries/03-jswidgets/01-cms-and-form-helpers.txt · Last modified: 2008/01/07 14:43 by aaron-n