Difficulty: moderate
Framework: MooTools 1.3 (no previous ver compatibility)
Dependencies: MooTools-more 1.3.1 (no compat mode, just Element.Delegation required to build it)
DocType: HTML5 – optional
Before 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 level.
I imagine you’d want to see what the end result will will look like before you waste your time reading so:
http://jsfiddle.net/dimitar/Ea4Gp/show/ – the Base Class, cross-fading
http://jsfiddle.net/dimitar/Ea4Gp/5/show/ – the Extended Class with alternative transition effect
http://jsfiddle.net/dimitar/Ea4Gp/5/ – the editable fiddle to play with.
Anyway. I just had cause to write a small class that allows me to swap between various content divs/panes that are part of the DOM and allow for any markup/child elements within. I thought I’d share the code as I could not readily find one that did things how I wanted them to be.
First, let us examine the requirements from a HTML point of view: a parent node (element) contains all the divs that will rotate. The Class needs to pick them up and enhance them. Should javascript be available, the user gets to see multiple content divs, otherwise–just the first one (progressive enhancement). Google has to be able to read text and follow any links within the content divs.
The resulting HTML markup looks like this:
<section class="rotator" id="rotator">
<div class="pane" style="background-image:url(http://fragged.org/img/home/hunting-home.jpg)">
<div class="info">
<h2>Hunting CS style</h2>
Built for your camping
pleasure.
</div>
</div>
<div class="pane hide" style="background-image:url(http://fragged.org/img/home/fishing-home.jpg)">
<div class="info">
<h2>Fishing? Really? </h2>
Fishing is for mongs. <a href="#">Click here</a>
</div>
</div>
<div class="pane hide" style="background-image:url(http://fragged.org/img/home/tourism-home.jpg)">
<div class="info rambling">
<h2>Rambling and walking</h2>
Wish you were here? Can't blame you, it's lame.
</div>
</div>
</section>
The accompanying CSS that goes along with the markup above:
section.rotator { border-bottom: 4px solid #000; width: 700px !important; height: 280px !important; overflow: hidden; position: relative; } section.rotator div.pane { position: absolute; width: 700px; height: 280px; background-repeat: no-repeat; background-position: left top; }
Come the MooTools magic. The Class is _very_ simple:
this.contentSwapper = new Class({ // mootools options and events mixins as standard Implements: [Options, Events], // some defaults... options: { delay: 3000, selector: "div", controlLeft: "http://fragged.org/img/home/moveLeft.png", controlRight: "http://fragged.org/img/home/moveRight.png" }, initialize: function(element, options) { // base constructor. // check to see if it's called with an element... this.element = document.id(element); if (!this.element) return; this.setOptions(options); // grab the child divs this.elements = this.element.getChildren(this.options.selector); this.index = 0; // call some methods we are about to define. this.attachControls(); this.startRotation(); this.attachEvents(); this.fireEvent("ready"); },
Once again, I like to break up all the things the Class does into small methods – ideally, as small as possible without getting into ridiculous territory where you write a method that returns a property you can access directly (eg, this.getSize = function() { return this.size; }).
attachControls is a cool method, as it makes use of some of MooTools 1.3’s new features: Slick and zen-like element construction.
attachControls: function() { this.controls = $$( new Element("img#moveLeft.contentControl[title=Previous][src={controlLeft}]".substitute(this.options)).inject(this.element, "top"), new Element("img#moveRight.contentControl[title=Next][src={controlRight}]".substitute(this.options)).inject(this.element, "top") ); },
Notice the $$() around the controls. This will create a MooTools HTML collection that is iteration-able (.each or any methods). The collection is basically like an array with added Element prototypes working. Other than that, nothing too complicated takes place – makes a new image with id #moveLeft, class .contentControl, sets the title and the src property.
To attach events: we want the rotation to stop when the user mouses over the main element (section.rotator) so they can read in peace and possibly click any call to action it may have.
attachEvents: function() { this.element.addEvents({ mouseenter: this.stopRotation.bind(this), mouseleave: this.startRotation.bind(this), "click:relay(img.contentControl)": this.move.bind(this) }); },
Hence, mousenter calls our stop method and mouseleave starts again. The “click:relay(img.contentControl)” is MooTools’ way to doing Event Delegation. Saves us you having to bind 2 events and callbacks to both controls by just binding to the parent element.
The movement methods. Very minimal logic, they literally just set variables.
move: function(e, el) { // based upon image id, it will match a method name. this[el.get("id")](); // call it dynamically }, moveLeft: function() { // set the next frame coming to previous one or last var next = (this.index == 0) ? this.elements.length-1 : this.index-1; this.swapFrames(next); this.fireEvent("left"); }, moveRight: function() { // set the next frame coming to next one or first var next = (this.index < this.elements.length-1) ? this.index+1 : 0; this.swapFrames(next); this.fireEvent("right"); },
Now we get to the more interesting parts. The actual swapping of the content is the core of the Class and is what we will modify later when we extend it to make it work differently.
The base class version does a VERY simple transition based around tweening the opacity of the 2 relevant content panes. One fades in while the other fades out.
swapFrames: function(next) { // element currently visible is in this.index (as key of this.elements array) var curEl = this.elements[this.index]; // clean up from before, just in case curEl.get("tween").removeEvents(); // reset element and set tween options. curEl.set({ "tween": { link: "cancel", onComplete: function() { // add a css class that sets display to none, for example this.element.addClass("hide"); } }, styles: { opacity: 1 // initial opacity reset in case quick clicks } }).fade(0); // now set our new visible frame from this.moveLeft/Right argument this.index = next; var newEl = this.elements[this.index]; //clean up, reset, show and fade in. newEl.get("tween").removeEvents(); newEl.setStyle("opacity", 0).removeClass("hide").fade(1); }
The end result: panes will cross-fade between each other, giving a morphing impression. Less is more, as they say.
Finally, we have 2 small methods that start and stop the automatic rotation. To keep the user interested in your site content, you really need to take no more than 2.5 second from their initial impression of the site or they may _subconciously_ lose interest and just bounce based upon the failure to connect with content shown from the start.
startRotation: function() { clearInterval(this.timer); this.controls.fade(.5); this.timer = this.moveRight.periodical(this.options.delay, this); this.fireEvent("start"); }, stopRotation: function() { clearInterval(this.timer); this.controls.fade(1); this.fireEvent("stop"); } // and end class... });
The controls collection we did earlier fades up and down based upon interaction: when it's stopped, the user is 'over' the element so we make the left and right more visible and we fade them out afterwards so they are less distracting.
This is it. To call the Class with your default options, all you need is:
new contentSwapper(document.id("rotator"));
You can, of course, attach event callbacks that respond to the ones the class has fired:
new contentSwapper.Fancy(document.id("rotator"), { onReady: function() { console.log("we are up!"); }, onStart: function() { console.log("and we are moving"); }, onStop: function() { console.log("Oh! they are reading with mouse on top!"); } });
... and so forth and so forth.
Simple. Now, imagine you are happy with your base Class but would like to extend it somewhat, by modifying it slightly to better suit your needs. In this case, we no longer desire to have a standard opacity cross-fade transition between panes so we will try to override that.
MooTools allows you to use the Extends: mutator(?) as part of your class declaration.
contentSwapper.Fancy = new Class({ Extends: contentSwapper, initialize: function(element, options) { this.parent(element, options); },
We have now extended the base class and have everything it does inherited. But we want to actually override one of the methods to make things snazzy. We will try to make a vertical transition of the new pane over the old one as an experiment.
swapFrames: function(next) { var curEl = this.elements[this.index]; // using morph for multiple CSS properties we will adjust // on the same timer, eg, opacity and margintop: curEl.get("morph").removeEvents(); curEl.set({ styles: { zIndex: 1000, opacity: 1 }, "morph": { link: "cancel", duration: 1000, onComplete: function() { this.element.addClass("hide"); } } }).morph({ // we really just want opacity as it looked better but experiment opacity: 0 }); this.index = next; // bring the new element in from the top, 280px. var newEl = this.elements[this.index]; newEl.get("morph").removeEvents(); newEl.removeClass("hide").setStyles({ zIndex: 1001, marginTop: -280, opacity: 0 }).morph({ marginTop: 0, opacity: 1 }); } }); // end extended class
That's it. Now, you can improve that further by setting the marginTop offset into the Fancy options or read the height of this.element instead, your call.
To use the alternative and fancier way of transitioning, you do:
new contentSwapper.Fancy(document.id("rotator"));
Supplemental CSS required:
img.contentControl { position: absolute; margin-top: 90px; z-index: 10000; cursor: pointer; _cursor: hand; } #moveLeft { margin-left: 0; } #moveRight { margin-left: 662px; }
Dead simple, isn't it? Have fun with MooTools. Any comments, questions or feedback, @D_mitar on Twitter or coda- on #mootools (irc.freenode.net).
incidentally, the 2 of the images used were stolen w/o permission and credit from flickr – oh well.