The other day somebody came in on the IRC channel and asked if there’s a MooTools script available to replicate the effect of this jQuery plugin.
The main objective was to be also easily able to keep the tip on whilst the mouse skips from the trigger element into it. Since I did not know off the top of my head of an adequate MooTools class to perform the task, I simply took 30 mins to write one. This is to document that and explain the decisions behind it.
To get an idea what our class will do when done, view http://jsfiddle.net/dimitar/WUfNR/show/ or look at the source code on git.
As a disclaimer: this is NOT a port of the jQuery plugin mentioned. I have not looked at the source code and simply am replicating the effect and using their graphics for convenience’ sake.
Also, this mini-tutorial or (even code-review) is aimed at people who already have rudimentary knowledge of Javascript, MooTools, CSS, DOM and understand the concept of classic OOP. Recommended read on how MooTools deals with OOP and the Class constructor in general is Keeto’s Up The Moo Herd III article.
First: decisions, decisions. Planning ahead is important so… The easiest way to achieve the effect we are after without firing onmouseout when moving the mouse into the ‘tip’ is to have the tip become a child element of the trigger element. To understand that better, consider it as a simple HTML/CSS markup solution where a child div has absolute positioning and some negative marginTop. Although this is very easy, it is a somewhat limiting decision as it means the tip element can only be injected into the DOM with a parentNode that can have children. input, img, textarea are not good for us to attach to directly. It’s not that much of a drama as you can wrap them into spans or divs, which will work fine.
The start: creating the MooTools class skeleton
this.tippable = new Class({ Implements: [Options, Events], options: { tipClass: ".tippable", }, initialize: function(element, options) { // public instantiation this.setOptions(options); this.element = document.id(element); if (!this.element) return; } });
What is happening here? Basically we are defining a new MooTools Class called ‘tippable’. Within that class we use the MooTools Options and Events classes as mixins. The Options class is useful for merging options from the instance that override the default class options property by doing an Object.merge(options, this.options);
. This functionality is available through the this.setOptions(options)
call and is a great practice that allows you to setup defaults that can be changed by the instance. The Events mixin is another very handy Class that brings Events (surprise) into your classes and can be used in conjunction with options object you pass on. The premise is simple: whenever something ‘awesome’ happens in your class that the instance should know about, you can do this.fireEvent(“awesome”, [params])
and if on your options object on your class instance you have a property onAwesome: function() { … }
, it will get passed with the scope of the class itself. More on this later.
Maintaining your MooTools Classes requires you to adhere to some unwritten rules that make it easier for developers to follow what’s going on. From that point of view, it’s an accepted convention that if your class deals with a particular element, it should be referenced as this.element
. An array of elements would be… this.elements
and so forth (hint: read the linked article above).
We are also making sure that an element has been passed and it’s a DOM node, something that the document.id()
call ensures. It’s unsafe for the Class to continue if it can’t find the element so it exits quietly. You may want to trigger an exception here or degrade gracefully – depends on your requirements.
Building your Class
It’s now time to add our first ‘real’ method. I like to break the Class down into multiple small methods that are self explanatory and do as little as possible. This is a great practice for many reasons: it means your code is self explanatory and reduces the need for leaving comments. It also allows you to create patterns that you can call again and again. We are going to create a new method we are going to call attachTip
attachTip: function() { // call creation and events addition this.createTip(); this.attachEvents(); },
As you can see, it will call two more methods we will add: one that creates the tip into the DOM and one that deals with the trigger events.
I am going to post the finished version of the method and it will reveal references to options and methods we have not mentioned yet. Basically – as a practice, I tend to add to my default options object as I see fit whilst I am writing the code and come to a decision about using a static value or a variable that may be useful for the user to override.
createTip: function() { // add tip to dom and set initials // first event will be onShow this.event = "show"; this.tip = new Element(this.options.tipClass, { styles: { opacity: 0 } }).set("morph", Object.merge(this.options.fx, { onComplete: function() { this.fireEvent(this.event); }.bind(this) })); // store title this.title = new Element("div.title").inject(this.tip); // tip body this.body = new Element("div.body").inject(this.tip); // append to DOM this.tip.inject(this.element, "top"); // store instance into the tip element for event use! this.element.store("tippable", this); // set initial title and body values this.setTitle(this.options.title); this.setHTML(this.options.text); // now to position element if (this.options.leftOffset === false) { var tipWidth = this.tip.getSize().x, elWidth = this.element.getSize().x; this.tip.setStyle("marginLeft", (elWidth - tipWidth) / 2); } else { this.tip.setStyle("marginLeft", this.options.leftOffset); } }, // end createTip
As I just pasted this, it occurred to me it would be useful to allow users to define their own CSS ClassNames for the tip title and tip body, even if they can be targeted in CSS as value-of-this.options.tipClass div.title {..}
. Anyway, that’s a change you can quickly do. Similarly, we are going to be using certain variables passed through options or defaults. The default options for the Class now look like this:
options: { // NB: these are zen classes for Slick tipClass: "div.tippable", text: "", textClass: "div.body", title: "", titleClass: "div.title", topOffset: 110, // where it stops above the element topOffsetStart: 160, // where the animation starts from. leftOffset: false, // if false, center over element, else, integer // Fx class options fx: { duration: 400, link: "cancel" } },
To reiterate: if you do not pass an options object to the Class instantiation, it will still work with the default values. If you want to override a single option, you do that and the rest are defaults. The key / interesting options we use are these:
- tipClass, textClass: a ‘zen-coding like’ element hook to create and style the tip container (by ID or CSS reference). In MooTools 1.3, Slick can work with Element in reverse, you can even declare things like
new Element(“div#someid.foo[html=hi]”);
- fx: an object that provides the ability to pass on options to the Fx.Morph instance on the tip element such as duration, Fx.transition and so forth.
- topOffset and topOffsetStart: can control the ‘animation’ path – where to start from and how far above (or below) the element to stop. By using negative values here, you can do a tip that flies from bottom to top instead…
- leftOffset: that value I added later when I realised I may not always want to try and center the tip above the trigger element but may want to output it to the left at nn pixels. if False, centered, else, marginLeft becomes leftOffset instead.
- text / title: Last but not least, the default text body and title for the tip.
Back to the function. In a nutshell, when attaching the tip to the DOM, it makes a new div, say div.tippable (styled through CSS alone) and makes it invisible by default. It also defines the Fx morphing options and helps out a little with the events it will fire onShow and onHide. I will go in more details over why events are important and how you can use them in the instance.
The tip is injected – as stated before – as a child node of the trigger element – so it needs to be styled via CSS to have position: absolute.
An interesting thing that I do here is I also store the tip instance into the trigger element storage (via this.element.store(“tippable”, this);). This allows me to programatically control the tip instance from external javascript by simply having access to the element where I can get the instance by var instance = doing elObj.retrieve(“tippable”);
. It’s great so that I don’t have to store each instance into a separate variable into my scope in case I want to remove it, modify it or show a tip without mouse events.
Another interesting thing is I have added public methods which allow for the changing of the tip title and body via setTitle and setHTML. This means you can control the content and change it dynamically or even bring in tip content through events and ajax, which I will show later on. Back to the Class, we are adding these two simple methods:
setTitle: function(what) { // set the title content public api if (what.length) { this.title.set("html", what); } return this; }, setHTML: function(what) { // set body content public api this.body.set("html", what); return this; },
Please note the return this; at the end of some of the methods. This is another unwritten rule in the MooTools world: allow for chaining. By having the Class instance as the exit/return point of a method, it allows you to chain more methods to it. In other words, you can do on a single line:
this.setTitle("hello").setHTML("this be the tip text, ahoy!");
When dealing with DOM events pointing back to your class methods, things are slightly trickier at first:
attachEvents: function() { // private this.boundEvents = { mouseenter: this.showTip.bind(this), mouseleave: this.hideTip.bind(this) }; this.element.addEvents(this.boundEvents); return this; }, detatchEvents: function() { this.element.removeEvents(this.boundEvents); return this; },
What takes place here is: on attachEvents we set mouseenter and mouseleave to point to the special methods we will create to show or hide the tip respectively. Because these two methods need to keep the scope to the class instance, we bind them to ‘this’. Because we need to be able to remove the tooltips from an element and clean up the events, we store a reference to the bound functions into the class instance so that detachEvents can find the callbacks and remove them (in this.boundEvents).
We are happy now, we can create the class and attach events to it. So here are the two functions that deal with showing and hiding the tip:
showTip: function() { // triggered on mousenter or called direct // fire onBeforeShow this.fireEvent("beforeShow"); // next event will be onShow through morph onComplete this.event = "show"; // animate properties this.tip.morph({ marginTop: [-this.options.topOffsetStart, -this.options.topOffset], opacity: [0, 1] }); return this; }, // end showTip hideTip: function() { // triggered on mouseleave ot called direct // fire onBeforeHide this.fireEvent("beforeHide"); // next morph event will be hide this.event = "hide"; // hide animation this.tip.morph({ marginTop: -this.options.topOffsetStart, opacity: [1, 0] }); return this; } // end hideTip
The first thing we do is we fire off the ‘before’ events. This can tell the instance we are about to start animating the tooltip into view or out of view. Why is this useful? You may want to modify your trigger element and make it an ajax spinner, change some content, modal your screen or even request data from ajax to show later.
this.event = “show”; is a little hook that allows us to pass back to the Fx.Morph onComplete event which event to fire next, in this case onShow. To the instance, that event says, ‘animation is complete and tip is in full view’. One thing you can do here is, for example, passing this to the options when instantiating the Class:
onShow: function() { this.setTitle("").setHTML("loading..."); var self = this; new Request({ url: "someurl.php", onComplete: function() { self.setHTML(this.response.text); } }).send("view=getTip&id=" + this.element.get("data-id")); }
… will fetch some data from an ajax view with some params and then set the tip body text to the response.
The hideTip fires the opposite events, onBeforeHide and onHide. An example use case for onHide can be to make it a one-off tip and remove the tippable after it has been seen:
onHide: { this.deatchEvents(); }
And done! Subsequent mouseenters on that element will no longer trigger a tooltip.
That’s it, in a nutshell. You have just created your new MooTools Class and can start using it. Look at the jsfiddle for this tutorial, it is somewhat important to look at the CSS that accompanies the Class:
http://jsfiddle.net/dimitar/WUfNR/. You can also see the events at work (look at your console) and try a a double-click on a trigger image to disable the class.
Any questions, you can always find me on #mootools on irc.freenode.net (nick coda- or d_mitar) or… just ask. I am sure somebody will be there to help. Have fun
thanks to Arian Stolwijk and Christoph Pojer from the mootools-core team for checking this for sanity and suggesting a few quick improvements.
[…] I begin, if you have not made a MooTools Class before, I suggest you first read this tutorial I made last month on creating a Sliding Tips plugin, which is a good entry […]
[…] **update** deprecated. On how to create a tooltip class, read this […]