Table of contents

Scenario

Sometimes, it is useful for text to be displayed when a user is in a form element, and hidden when they leave the field. An example would be a prompt which offers help about the expected field contents, but which generally doesn't need to be displayed, since displaying them all at once would clutter up the interface.

This article will cover how to achieve this in your wicket page.

Overview of Options

Three basic ways to achieve this functionality are:

Option 1. Create ajax behavior which gets fired when field is entered/left.

  • Pro: Most dynamic solution.
  • Con: Requires multiple round trips to server per user.
  • Con: Implementation is verbose.

Option 2. Tag each field with an hint attribute, and use javascript tools to display the text as needed.

  • Pro: Works as needed.
  • Con: Performed at the page level, needs components to be tweaked to match.

Option 3. Wrap #2 inside of appropriate wicket components/behaviors.

  • Pro: Achieves goal, and works within components.
  • Con: Additional work needed over #2 for initial implementation.

Implementations

Option 1: Create ajax behavior which gets fired when field is entered/left.

I believe this can be achieved using AjaxFormComponentUpdatingBehavior. However, after reviewing the cons, I chose not to implement it in this manner. Someone else can certainly update this if they have had success using this method.

Option 2: Tag each field with an hint attribute, and use javascript tools to display the text as needed.

You could use any javascript library here. I am going to demonstrate using the Mootools library because it is small, modular, and does what is needed. Feel free to expand this with other options if you are familiar with them.

Here is a brief overview of the steps we will need to complete to implement this functionality:

  1. Obtain Mootools library.
  2. Customize Mootools library with a custom class.
  3. Modify our page markup.
  4. Execute the javascript function when our page loads.

And in detail:

  1. Obtain Mootools library.
    • Download the mootools library from Mootools.net.
      • The following packages must be selected for download, at a minimum (as of Mootools.v1.00).
        • Tips (and all prerequisites)
        • Window.Base
        • Dom
      • You can choose compressed or not. Typically, you'll want uncompressed until we complete our next step, and compressed when in production.
  2. Customize Mootools library with a custom class.
    • The Tips class displays a tooltip when the mouse goes over an html element of a specified class. This is similar to what we want to do, but not quite, so we will create a new class which does what we want.
    • Create a new .js file. We'll use Hints.js here.
    • If you downloaded the uncompressed version of mootools above, you can copy and paste the tips class into your own file, and make some modifications to it to achieve our desired behavior.
      1. Class name needs to be changed. In our case, we will use Hints as the class name.
        var Hints = new Class({
        
      2. Update the getOptions block. We want to add a new option, sizeOffset.
        getOptions: function(){
            return {
                onShow: function(tip){
        	    tip.setStyle('visibility', 'visible');
        	},
        	onHide: function(tip){
        	    tip.setStyle('visibility', 'hidden');
        	},
        	maxTitleChars: 50,
        	showDelay: 100,
        	hideDelay: 100,
        	className: 'tool',
        	offsets: {'x': 0, 'y': 3},
                sizeOffset: { 'x': false, 'y': true },
                fixed: true
            };
        },
        
      3. Next, we want to update the build funtion. We make several changes here. First, we display only the text, no title. Then we define the property we want this class to use as its text. Last, we change the events to focus and blur, so that our text is show when our input fields are entered and left, rather than on mouseover. Note that event of locate and start in the focus event differs from in the Tips declaration. This is due to the positioning code required.
        build: function(el){
            el.myTitle=false;
            el.myText=el.getAttribute("tooltip");
        
            el.addEvent('focus', function(event) { this.locate(el); this.start(el); }.bindWithEvent(this));
            el.addEvent('blur', this.end.bindWithEvent(this));
        },
        
      4. The next part of the Hint code that is different from the Tips code is the locate section. This section determines where the text will be displayed, and obviously this is quite different since we are not rendering at the mouse location. We use the offset and sizeOffset options to help us position the code around the control. Use the comments for guidance here.
        locate: function(el){
        
            // Locate where the event came from.  In our case:
            //  position is the upper left corner of the input that gained focus
            //  size is the size of the input that gained focus, in px
            var pos=el.getPosition();
            var size=el.getSize();
        
            // By default, position hint at top left corner of input,
            // and offset by our offset amounts.
            var left = pos['x'] + this.options.offsets['x'];
            var top = pos['y'] + this.options.offsets['y'];
        
            // If user specified that a sizeOffset should be used, additionally
            // offset the hint by the size of the control that gained focus
            if (this.options.sizeOffset['x']){ left = left + size['size']['x']; }
            if (this.options.sizeOffset['y']){ top = top + size['size']['y']; }
        
            // Assign the hint location
            this.toolTip.setStyle('left',  left + 'px');
            this.toolTip.setStyle('top', top + 'px');
        },
        
      5. Finally, don't forget to update the initialization code
        Hints.implement(new Events);
        Hints.implement(new Options);
        
    • In the end, that leaves us with the following Hints.v0.5.js file:
      var Hints = new Class({
      
          getOptions: function(){
              return {
      	    onShow: function(tip){
      	        tip.setStyle('visibility', 'visible');
      	    },
      	    onHide: function(tip){
      	        tip.setStyle('visibility', 'hidden');
      	    },
      	    maxTitleChars: 50,
      	    showDelay: 100,
      	    hideDelay: 100,
      	    className: 'tool',
      	    offsets: {'x': 0, 'y': 3},
                  sizeOffset: { 'x': false, 'y': true },
                  fixed: true
      	};
          },
      
          initialize: function(elements, options){
              this.setOptions(this.getOptions(), options);
      	this.toolTip = new Element('div').addClass(this.options.className+'-tip').setStyles({
      	    'position': 'absolute',
      	    'top': '0',
      	    'left': '0',
      	    'visibility': 'hidden'
      	}).injectInside(document.body);
      	this.wrapper = new Element('div').injectInside(this.toolTip);
      	$each(elements, function(el){
      	    this.build($(el));
      	}, this);
      	if (this.options.initialize) this.options.initialize.call(this);
          },
      
          build: function(el){
              el.myTitle=false;
      	el.myText=el.getAttribute("tooltip");
      
      	el.addEvent('focus', function(event) { this.locate(el); this.start(el); }.bindWithEvent(this));
      	el.addEvent('blur', this.end.bindWithEvent(this));
          },
      
          start: function(el){
      	this.wrapper.setHTML('');
      	if (el.myTitle){
      	    new Element('span').injectInside(
      	        new Element('div').addClass(this.options.className+'-title').injectInside(this.wrapper)
                  ).setHTML(el.myTitle);
      	}
      	if (el.myText){
      	    new Element('span').injectInside(
      	        new Element('div').addClass(this.options.className+'-text').injectInside(this.wrapper)
      	    ).setHTML(el.myText);
      	}
      	$clear(this.timer);
      	this.timer = this.show.delay(this.options.showDelay, this);
          },
      
          end: function(event){
      	$clear(this.timer);
      	this.timer = this.hide.delay(this.options.hideDelay, this);
      	event.stop();
          },
      
          locate: function(el){
      
              // Locate where the event came from.  In our case:
              //  position is the upper left corner of the input that gained focus
              //  size is the size of the input that gained focus, in px
              var pos=el.getPosition();
      		var size=el.getSize();
      
              // By default, position hint at top left corner of input,
              // and offset by our offset amounts.
              var left = pos['x'] + this.options.offsets['x'];
              var top = pos['y'] + this.options.offsets['y'];
      
              // If user specified that a sizeOffset should be used, additionally
              // offset the hint by the size of the control that gained focus
              if (this.options.sizeOffset['x']){ left = left + size['size']['x']; }
              if (this.options.sizeOffset['y']){ top = top + size['size']['y']; }
      
              // Assign the hint location
              this.toolTip.setStyle('left',  left + 'px');
              this.toolTip.setStyle('top', top + 'px');
          },
      
          show: function(){
              this.fireEvent('onShow', [this.toolTip]);
          },
      
          hide: function(){
              this.fireEvent('onHide', [this.toolTip]);
          }
      
      });
      
      Hints.implement(new Events);
      Hints.implement(new Options);
      

MooTools 1.2

Note that when using current (1.2) MooTools, you´d have to replace any

setHTML(

by

set('html',
  1. Modify our page markup.
    • Next, we need to ensure that our javascript knows what to mark up, and what to mark it up with.
      1. Add a style class to the inputs you want to have a hint. In our case, we will use "hinted". For example:
        <input class="hinted" wicket:id="usernameField" id="username" type="text"/>
        
      2. Add an attribute that has the text you want displayed for each input element. This has to match the attribute specified in the Hints build function. In our example, we use "tooltip".
        <input class="hinted" wicket:id="usernameField" id="username" type="text" tooltip="Remember, usernames are case sensitive."/>
        
  2. Execute the javascript function when our page loads.
    • Now that we have the java script and markup in place, we just need to use them on page load.
    • To achieve this, the script file needs to be added to the page header, and then the function needs called on page load.
      1. Add the script to the page header.
        <head>
        ...
        <script src="Hints.v0.5.js"></script>
        </head>
        
      2. Call the function on page load. In this example, any input element with the class "hinted" will display a Hint offset 10px off its right side, and 4 pixels from its top, whose text will be the input's "tooltip" attribute value.
        <head>
        ...
        <script src="Hints.v0.5.js"></script>
        <script type="text/javascript">
            window.addEvent('domready', function(){
                var myHints = new Hints($$('.hinted'), {
                offsets: {'x': 10, 'y': 4},
                sizeOffset: {'x':true, 'y':false }
              });
            });
        </script>
        </head>
        

Option 3: Wrap Implementation #2 inside of appropriate wicket components/behaviors.

Lets start off by creating a base behavior for all other mootools behaviors. This behavior will include the base mootools.v1.00.js file in the page.

Create package wicket.mootools and wicket.mootools.res. The res package will contain various resources. Put the mootools.v1.00.js into that package.

Now the behavior:

package wicket.mootools;

import wicket.Component;
import wicket.ResourceReference;
import wicket.behavior.AbstractBehavior;
import wicket.markup.ComponentTag;
import wicket.markup.html.IHeaderContributor;
import wicket.markup.html.IHeaderResponse;

public class AbstractMooToolsBehavior extends AbstractBehavior implements  IBehavior {

	// create a reference to the base mootools javascript file.
        // we use JavascriptResourceReference so that the included file will have its comments stripped and gzipped.
        private static final ResourceReference MOOTOOLS_JS = new JavascriptResourceReference(AbstractMooToolsBehavior.class,
		"res/mootools.v1.00.js");

	public void renderHead(IHeaderResponse response) {
		// include the mootools js in the page's head
                response.renderJavascriptReference(MOOTOOLS_JS);
	}

	/** helper method that hooks into mootools ondomready event */
        protected String executeOnWindowDomReady(String script) {
		StringBuilder builder = new StringBuilder(script.length() + 61);
		builder.append("<script>window.addEvent('domready', function(){\n");
		builder.append(script);
		builder.append("\n});</script>");
		return builder.toString();
	}

}

Now lets take a look at our specific case of adding tooltips. The tooltips need a background image called bubble.png, put that into the res package. They also require a bit of css, here is what it looks like:

<style>
.tool-tip {color: #fff; width: 516px; z-index: 13000;}
.tool-title {font-weight: bold; font-size: 11px; margin: 0; padding: 8px 8px 4px; background: url(bubble.png) top left;}
.tool-text {margin: 0; font-size: 11px; padding: 4px 8px 8px; background: url(bubble.png) bottom right;}
</style>

Notice the css references bubble.png, so we will need to somehow make the css point to the right url. Lets template the css, so we can substitute the right url at runtime. Create a file in the res package called ToolTipsCssTemplate.css with the following contents:

<style>
.tool-tip {color: #fff; width: 516px; z-index: 13000;}
.tool-title {font-weight: bold; font-size: 11px; margin: 0; padding: 8px 8px 4px; background: url(${bubble-url}) top left;}
.tool-text {margin: 0; font-size: 11px; padding: 4px 8px 8px; background: url(${bubble-url}) bottom right;}
</style>

We substituted the reference to bubble.png with ${bubble-url} variable, we will make wicket replace it for us at runtime with the url for the image.
Copy Hints.v0.5.js into the res package

Now lets create the tooltips behavior

package wicket.mootools;

import java.util.Map;

import wicket.RequestCycle;
import wicket.ResourceReference;
import wicket.markup.html.IHeaderResponse;
import wicket.util.collections.MicroMap;
import wicket.util.resource.PackagedTextTemplate;

public class MooToolsFixedTooltips extends AbstractMooToolsBehavior {

	private static final ResourceReference BUBBLE_PNG = new ResourceReference(AbstractMooToolsBehavior.class,
		"res/bubble.png");

	private static final ResourceReference FIXEDTIPS_JS = new JavascriptResourceReference(AbstractMooToolsBehavior.class,
		"res/Hints.v0.5.js");

	/** render the necessary javascript to enable tooltips */
        @Override
	public void renderHead(IHeaderResponse response) {
		// allow the base behavior to do what it needs to
                super.renderHead(response);
                // include a link to our modified tooltips js
		response.renderJavascriptReference(FIXEDTIPS_JS);
		// include the css
                response.renderString(css());
		response
			.renderString(executeOnWindowDomReady("var myHints = new Hints($$('.hinted'), "
                            +"{offsets: {'x': 10, 'y': 4}, sizeOffset: {'x':true, 'y':false }"));
	}

	/** create the css string with the properly replaced bubble-url variable */
        private String css() {
		// load the css template we created form the res package
                PackagedTextTemplate template = new PackagedTextTemplate(MooToolsFixedTooltips.class,
			"res/ToolTipsCssTemplate.css");
                // create a variable subsitution map
		Map<String, CharSequence> params = new MicroMap<String, CharSequence>("bubble-url", RequestCycle.get().urlFor(
			BUBBLE_PNG));
                // perform subsitution and return the result
		return template.asString(params);
	}
}

Now all you need to do is add this behavior to the page or to any component and add appropriate tooltip and class to any tag.

  • No labels