Apr 7 2010

More than one way… (delegate edition)

There was a question in the forums about affecting non-hovered items. The effect they were after is that they had an unordered list of items and when they were rolled over, they would all dim (lower opacity) except the one hovered.

This can be done with CSS, using pseduo-selectors.

ul li:not(:hover) { opacity: 0.5; }

However we know that pseudo-selectors don’t have very good cross-browser support. And for that matter, opacity doesn’t either. jQuery is pretty good at mitigating cross-browser problems, so l thought I might give that a spin. In attempting it, I had a nice little learning journey.

My first thought is that I needed to write a selector to select all list items except the one currently being hovered over. I had this come up recently for another reason, and I made a snippet for it: Excluding this from the selector. In this example:

$("ul li").not(this).css("opacity", "0.5");

We just need to wrap that in some kind of hover function. The most obvious:

$("li").hover(function() {
  $("li").not(this).css("opacity", 0.5);
}, function() {
  $("li").not(this).css("opacity", 1);
});

But I’ve been being taught that binding events like this is inefficient, since 1) it requires one event handler for every single element and 2) new elements appended to the page after this code runs will need to be re-bound. (Not to mention, you would definitely want to cache that selector above var $listItems = $("li");).

So I thought I’d go right for the new delegate function which we have mentioned here on CSS-Tricks a few times now. This is awesome because it solves both the issues mentioned above. This was my first (utterly broken) attempt.

$("ul").delegate("li", "hover", function() {
    $("ul li").not(this).css("opacity", "0.5");
}, function() {
    $("ul li").not(this).css("opacity", "1");
});

Don’t try that at home, it’s not going to work. Why not? James Padolsey reminded me that “hover” isn’t an event. It’s a jQuery function, but not a real event, and will not work with delegate. Besides, even if it did, delegate is expecting just the three parameters: element, event, and function, not four parameters like I was passing it (assuming it would know the last function was supposed to be a callback/mouseleave).

Then the next most obvious transformation becomes this:

$("ul").delegate("li", "mouseenter", function() {
    $("ul li").not(this).css("opacity", "0.5");
}).delegate("li", "mouseleave", function() {
    $("ul li").not(this).css("opacity", "1");
});

That uses two delegate functions, this time with real events, to get the job done. This is fine, but we can make it a bit more efficient by mapping both events to a single delegate and then just testing to see what type of event was fired. David Link had this idea:

$("ul").delegate("li", "mouseover mouseout", function(e) {
    if (e.type == 'mouseover') {
      $("ul li").not(this).css("opacity", "0.5");
    } else {
      $("ul li").not(this).css("opacity", "1");
    }
});

Which James Padolsey had an even cleaner version:

$("ul").delegate("li", "mouseover mouseout", function(e) {
    $("ul li").not(this).css("opacity", e.type == 'mouseover' ? 0.5 : 1);
});

jQuery’s live() function is also a good choice here, but has some quirks as well. It turns out you can pass “hover” to live, but you can still only provide a single function. The function will then fire on both mouseenter and mouseleave events, and you’ll have to do event.type testing (like above) to figure out which it was and behave accordingly. Thanks to Paul Irish and Jeffrey Way for that one.

And remember that CSS selector from the very top? We can use that right in jQuery too:

$("ul li:not(:hover)").css("opacity", "0.5");

Quite the journey eh? Like all things web, always more than one way to skin the cat.


Apr 7 2010

jQuery Conference + Win a Ticket!

I’ll be speaking at the upcoming jQuery Conference. It is in the San Francisco Bay Area (Mountain View, at the Microsoft Campus) on April 24 – 25, 2010. Just check out that link for all the details. It looks pretty great.

“Solving Common Client Requests with jQuery”

That is my chosen topic. Since I think I’ll be speaking to a room of folks 75% of which are going to be smarter than me, I thought I would take more of a real-world approach rather than straight instructional. We’ll be taking a look at some actual sites I’ve worked on and how I leaned on jQuery to help me accomplish something that would have been difficult or impossible otherwise.

Win a free ticket!

I have one free ticket to give away to a CSS-Tricks reader. I always struggle with this because I don’t like “leave a comment to win” contests and I also dislike Facebook/Twitter related contests. So we are going to try something new here.

To enter to win the free ticket: you must donate $10 to ANY open source project. Then forward your receipt to me at chriscoyier@gmail.com and you’ll be entered to win.

If you win, you get a $199 ticket for $10. If you lose, you get the intense satisfaction that you helped an deserving open source project. If you aren’t comfortable with that, don’t enter. Transportation and lodging is up to you.

Also remember it’s ANY open source project. Could be jQuery (or some plugin), could be WordPress (or some plugin), or literally ANY other open source project that accepts donations.

This is going to be fairly quick. I’m going to pick winners on this Friday, April 9 2010.


Apr 6 2010

Slot Machine Tabs

I was looking at the features page of a web service called Fluxiom. I haven’t used the product (although it looks pretty nice and might be good few a couple of our clients). It’s the tabs on that page that I thought were pretty neat. As you click a different tab, the three columns of text fly upward at different rates and are replaced by new columns. It looks kinda like a slot machine. I didn’t investigate too deeply how they were doing it, but as I often do, I set about recreating the effect with jQuery.


After clicking a new tab the three columns slide away and are replace with new ones at random rates, like a slot machine.

View Demo   Download Files

I thought I did OK… although it can definitely be improved. There are also enough interesting things to talk about, so let’s get after it.

HTML

Just going to do a bit of a code dump here so you can see it all.

<div id="slot-machine-tabs">

	<ul class="tabs">
		<li><a href="#one">Tab One</a></li>
		<li><a href="#two">Tab Two</a></li>
		<li><a href="#three">Tab Three</a></li>
	</ul>

	<div class="box-wrapper">

		<div id="one" class="content-box">
			<div class="col-one col">
				<img src="images/evangeline.jpg" alt="" />
			</div>
			<div class="col-two col">
				<h3>Kate</h3>
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
			<div class="col-three col">
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
		</div>

		<div id="two" class="content-box">
			<div class="col-one col">
				<img src="images/elizabeth.jpg" alt="" />
			</div>
			<div class="col-two col">
				<h3>Juliet</h3>
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
			<div class="col-three col">
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
		</div>

		<div id="three" class="content-box">
			<div class="col-one col">
				<img src="images/sonya.jpg" alt="" />
			</div>
			<div class="col-two col">
				<h3>Penelope</h3>
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
			<div class="col-three col">
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
		</div>

	</div> <!-- END Box Wrapper -->

</div> <!-- END Slot Machine Tabs -->

Important points:

  • The whole thing is wrapped in an div with an ID value. Should we convert this idea into a plugin, the idea would be to target this ID and do the magic. But the ID is totally unnecessary for the CSS. That means we could have multiple instances of the slot machine tabs on a single page.
  • The “navigation” (the tabs) are at the top above the content boxes. With styling turned off, this will look like a navigation list like any other. The href values for the links point to the ID’s of the content boxes, so the links would jump down the page to the corresponding content. Ideal.
  • When there are a lot of closing </div>’s in a row, I like to do the thing where you add a comment afterward to explain which thing this is closing (e.g. <!– END page wrap –>)

CSS

The tabs themselves will be like any other horizontal navigation. We’ll make the list items inline and the anchors block floated left. Simple borders and backgrounds, and a special state for “current” (no border and bumped down) and we’re set.

.tabs { list-style: none; overflow: hidden; padding-left: 1px; }
.tabs li { display: inline; }
.tabs li a { display: block; float: left; padding: 4px 8px; color: black; border: 1px solid #ccc; background: #eee; margin: 0 0 0 -1px; }
.tabs li a.current { background: white; border-bottom: 0; position: relative; top: 2px; z-index: 2; }

One somewhat-unsemantic thing we are using is the #box-wrapper div, but whatever it’s not that bad and it helps us in a number of ways. For one, it’s the the relative positioning container which limits the scope of absolute positioning inside it. We can then absolutely position each content box on top of each other inside.

My favorite part is the box-shadow CSS around the #box-wrapper div. Not only does it help the tabbed area pop up a bit, but it handles the shading on the tabs themselves. The box wrapper sits on top (literally, z-index wise) of the non-current tabs. This is exactly the illusion/visual-metaphor we are going for: the current tab is on top and connected, the non-current tabs are “behind” (shadow is cast upon them).

.box-wrapper { -moz-box-shadow: 0 0 20px black; -webkit-box-shadow: 0 0 20px black; padding: 20px; background: white; border: 1px solid #ccc; margin: -1px 0 0 0; height: 210px; position: relative; }
.content-box { overflow: hidden; position: absolute; top: 20px; left: 20px; width: 658px; height: 230px; }

Notice we also pull the box up by one pixel with a negative top margin. That makes sure the under-border of the non-current tabs don’t make a 2px line but a consistent 1px line.

Another important empowering concept here is that the #box-wrapper has hidden overflow and a set height. The columns in all the content boxes that are non-current are hidden by way of pushing their top value to 350px (a value taller than the height of the box). This pushes them completely out of view because of the hidden overflow. JavaScript will later do the job of pulling up new columns and pushing the old ones out of the way when needed.

The columns:

.col { width: 30%; float: left; position: relative; top: 350px; }
.col-one, .col-two { margin-right: 3%; }

Notice we only apply right margin to the first two columns. Another way to have done that is to apply the margin to all the columns but use .col:last-child { margin-right: 0; } to remove it. That might be the best way to go if you plan on having a variable number of columns. Just be aware of the lack of pseudo selector support on IE.

jQuery JavaScript

This isn’t plugin-ized yet, but it probably could/should be. There are some things I would want to fix/make less redundant before that happens, which I’ll cover later. You can view the full commented JavaScript file here.

I’m not going to do a full code dump but I’ll cover some interesting lines.

Right away the first tab and the first content box are declared as current. The current content columns are moved to a top position of 0 (so they are visible) rather than the default hidden value from the CSS.

$(".tabs li:first-child a, .content-box:first").addClass("current");
$(".box-wrapper .current .col").css("top", 0);

We use the delegate function for the click events on the tabs, since that’s so efficient (and could handle dynamic addition of tabs if that came up):

$("#slot-machine-tabs").delegate(".tabs a", "click", function() {
   // stuff
}

When a click happens, action only takes place if the tab clicked on is not the current tab and there is no other animation taking place on the page. In this limited demo, the only animation possible is the columns. In a more “real” environment the scope of this test should probably be pared down to inside the slot machine tabs specific ID. Something like $("#slot-machine-tabs *:animated)

$el = $(this);

 if ( (!$el.hasClass("current")) && ($(":animated").length == 0 ) ) {
    // stuff
}

I thought it was a more engaging effect if columns didn’t change at the same rate each time. I set the speeds pseudo-randomly for each animation, but with a base value of half a second. The speed for the leaving column and entering column match though, so there is no overlapping.

speedOne = Math.floor(Math.random()*1000) + 500;
speedTwo = Math.floor(Math.random()*1000) + 500;
speedThree = Math.floor(Math.random()*1000) + 500;

Notice in the demo how all new columns always slide up from the bottom. But as they leave, the slide up to the top. That is accomplished because after the slide-them-up-and-away animation is finished, we instantly move them back down to the default low-and-hidden position. Before doing that, we need to make sure that all animations are completed. I had a slightly hard time doing this. Normally to fire something after an animation, it’s no big deal because you can have a callback function on an animation which only fires when the animation is complete. But we are running six different animations here and because they all take a random length of time, we don’t know which one is going to end last.

My solution so far is to call the same function on callback on every single finishing animation. For example:

$(".box-wrapper .current .col-one").animate({
	"top": 0
}, speedOne, function() {
	ifReadyThenReset();
});

The ifReadyThenReset() function will only do it’s thing (reset the column top positions) when it’s been called for the third time:

var columnReadyCounter = 0;

function ifReadyThenReset() {

	columnReadyCounter++;

	if (columnReadyCounter == 3) {
		$(".col").not(".current .col").css("top", 350);
		columnReadyCounter = 0;
	}

};

Issues:

  • It’s not a plugin.
  • Each of the columns involved in a tab-changing event is individually animated. That’s six things being animated at once and all of them are “hard-coded”. This makes adding or removing columns more of a chore than it should be.
  • The call-the-callback-three-times thing seems kludgey to me, and the hard coded three shares the same problem as above.

Demo & Download

View Demo   Download Files

As always, do what you will with this, including use it in a corporate project to impress your boss and use as example of why you deserve a raise.


Apr 3 2010

The Abstraction Point

Reader Joe Bob sent me a link to IxEdit to ask what I thought. I hadn’t heard of it, so I checked it out. They have a six minute video you can watch which explains it pretty well. In a nutshell, it’s a GUI editor for creating interactive stuff on websites. Click an element, tell it how you want it to behave. Think of it like CSS Edit for jQuery (it uses jQuery and jQuery UI to do it’s thing).

My thoughts: Wow, that’s extremely cool. But I’d probably never use it.

The fact that I’d never use it has nothing to do with the quality of the product. It has to do with the fact that it is beyond my Abstraction Point. If I’m going to write some page interactions, like a tabbed area or a click-this-slide-down, I’d rather write it myself in jQuery. But that’s kind of absurd isn’t it? jQuery in itself is an abstraction of JavaScript. When I write $(“#thingy”).slideDown(); there is lots of stuff going on that I barely understand. I’m far from “writing it from scratch”.

Taking it further, that code is ultimately served up from a web server. I certainly didn’t write that code. That web server runs on an operating system. The operating system runs on lower level code. There is a lot of steps between that circuit board and my slideDown animation. Everybody stops along the chain somewhere.

“That’s for newbies, not real developers.”

“I write my code from scratch!”

“I like to know what my code is doing.”

I think these are flawed statements. When we say or think these things, we are really saying “This is above my abstraction point.”

Your abstraction point is the level of abstraction you feel most comfortable in getting stuff done from day to day. It might be using design view in Dreamweaver and adding interactions with IxEdit. It might be writing in assembly language. Neither one is “better” than the other. It’s just where your current comfort level is, and more importantly, using the tools you need to to get the task done with reasonable speed.


Apr 3 2010

New Screencast: Thoughts on SEO

Fair warning: more rambling than usual. Listen to my thoughts about SEO. What I think I know is that SEO is a series of fairly obvious best practices. A SEO service that helps you with those things can be good, a SEO service that claims to do anything else seems shady. However, there are plenty of examples where seemingly doing everything right doesn’t seem to work, and results that show up higher than your project look like pure garbage. SEO is, and may always be, quite mysterious.


View Screencast