Skip to content

Instantly share code, notes, and snippets.

@felixge
Created October 27, 2010 15:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save felixge/649208 to your computer and use it in GitHub Desktop.
Save felixge/649208 to your computer and use it in GitHub Desktop.
gc.widget("gc.cl.widgets.scrollable", {}, function (m, document, $, gc) {
$ = jQuery; // Sorry Malte
var ANIMATION_SPEED = 800;
// A little helper function to sniff common CSS properties that we need
// to animate form an event.
function getCss($element) {
var css = {
opacity: $element.css('opacity'),
width: $element.outerWidth(),
height: $element.outerHeight(),
marginLeft: $element.css('marginLeft'),
marginTop: $element.css('marginTop'),
marginRight: $element.css('marginRight'),
marginBottom: $element.css('marginBottom')
};
return css;
}
// A little helper giving us a callback when an image is really loaded
function onLoad(imageUrl, fn) {
var notify = function() {
$loader.remove();
fn();
},
$loader = $('<img />')
.attr('src', imageUrl)
.appendTo('body')
.hide();
// Detect whether the image has been cached. IE7 reports 28px width() on images
// that have no yet been loaded, I this is the size of the image-not-found
// icon they use.
if ($loader.width() > 28) {
notify();
return;
}
$loader.one('load', notify);
}
this.nodeReady = function(node) {
node.find('.js_scrollable').each(function() {
var $scrollable = $(this),
config = $scrollable.dataset(),
$list = $scrollable.find('.js_scrollableitems'),
$items = $list.children(),
$detail = null,
$detailImage = null,
detailCss = null,
detailShowCss = null,
$selectedItem = null,
itemWidth = null,
selectedItemWidth = null,
$spinner = null,
state = null,
direction = null;
// Called once, initiales a few variables and stuff for each scrollable
function init() {
// Determine itemWidth and selectedItemWidth
var $first = $items.first();
itemWidth = $first.outerWidth();
$first.addClass('selected');
selectedItemWidth = $first.outerWidth();
$first.removeClass('selected');
// Determine if there is a detail view (used for looksdetails)
$detail = jQuery(config.detailselector);
if (!$detail.length) {
$detail = null;
} else {
$detailImage = $detail.find('img');
$detail.addClass('show');
detailShowCss = getCss($detail);
$detail.removeClass('show');
detailCss = getCss($detail);
if (!$.support.opacity) {
// Sniffing opacity doesn't seem to work in IE. Not even when setting
// the Alpha Transform filter crap. So we'll have to hard code it here : /
detailShowCss.opacity = 1;
detailCss.opacity = 0;
}
}
// Load the spinner image if any
var spinnerUrl = config.spinnerurl;
if (spinnerUrl) {
$spinner = $('<img class="spinner" />')
.attr('src', spinnerUrl)
.appendTo('body')
.hide();
}
// Set item click listener
// We are not using delegate here in order to not make any assumptions
// about the type of element $items are made up of
$list.bind('click', function(e) {
var $item = $(e.target), x = 0;
while (true) {
if ($list.children().filter($item).length) {
break;
}
$item = $item.parent();
}
select($item);
});
// Handle mouse scroll events
$scrollable.bind('mousewheel', function (e, delta, deltaX, deltaY) {
move(deltaX);
});
// Handle next/prev arrows
$scrollable.delegate('.js_prev, .js_next', 'click', function() {
if ($(this).is('.js_next')) {
move(1);
} else {
move(-1);
}
});
// Cut our list in 2 and insert the second half in the beginning
if (config.centerfirst === 'true') {
$items
.slice($items.length / 2)
.insertBefore($first);
$items = $list.children();
}
setListWidth();
if (config.triplet === 'true') {
// Select the 2nd element for triplets like the Adidas slideshow
$first = $first.next();
}
// Set the initial position and select the first element
setListLeft($first);
select($first);
}
function move(offset) {
if (!offset) {
return;
}
var $next = (offset < 0)
? $selectedItem.prev()
: $selectedItem.next();
if (!$next.length) {
// This can happen from fanatic mouse scrolling
// in this case let's wait for the screen to catch up before trying to move again
return;
}
select($next);
}
// Makes our list element wide enough to fit all elements, even when one of them is selected
function setListWidth() {
var width = ($list.children().length - 1) * itemWidth + selectedItemWidth;
$list.css('width', width);
}
// Positions our list so the selected item is centered
function setListLeft($centerItem, selected) {
$list.css('left', calculateListLeft($centerItem, selected)+'px');
}
function calculateListLeft($centerItem, selected) {
var screenWidth = $(window).width()
centerItemWidth = itemWidth,
itemOffset = 0;
if (selected) {
centerItemWidth = selectedItemWidth;
}
if ($centerItem) {
itemOffset = $centerItem.position().left;
}
return (screenWidth - centerItemWidth) / 2 - itemOffset;
}
function select($item) {
var $previouslySelectedItem = $selectedItem;
function addInfiniteScrollItems() {
if (!direction) {
// If we didn't move, no need to add items
return;
}
var remainingItems = null,
fill = null,
neededItems = Math.ceil($(window).width() / itemWidth / 2);
// Figure out how many items are remaining in the list depending on the
// direction we are currently moving in. `insert` holds the method we
// would use if we needed to add more elements in that direction
if (direction == 'right') {
remainingItems = $selectedItem.nextAll().length;
insert = 'appendTo';
} else if (direction == 'left') {
remainingItems = $selectedItem.prevAll().length;
insert = 'prependTo';
}
if (remainingItems < neededItems) {
// If we do not have enough items to fill the screen, add a complete batch of new
// ones to the list
$items
.clone()
[insert]($list);
// After that we need to reset the list width and position
setListWidth();
setListLeft($previouslySelectedItem);
}
}
function animatePrevioslySelected() {
if (state == 'animatePrevioslySelected') {
// If we are already fading the old image out, we just need to set
// the new $selectedItem target.
$selectedItem = $item;
return;
}
if (state == 'scrollToSelected' || state == 'waitForDetailImage') {
// If we are already scrolling to a selected image, we just need to
// set the new $selectedItem target. scrollToSelected takes care of
// stopping the current animation.
$selectedItem = $item;
scrollToSelected();
return;
}
if (state == 'animateSelected') {
// If we are in the process of fading a new image in, we are simply
// making that image the previouslySelectedItem, so the animation
// is stopped and we reversed in this function.
$previouslySelectedItem = $selectedItem;
}
state = 'animatePrevioslySelected';
$previouslySelectedItem.removeClass('selected');
if (!$detail) {
// Not much todo here for regular scrollables
$selectedItem = $item;
scrollToSelected();
return;
}
// Here we are resizing the selected image container back to be small
$previouslySelectedItem
.stop()
.animate({width: itemWidth}, ANIMATION_SPEED);
// Fade the small image back in
$previouslySelectedItem
.find('img')
.stop()
.animate({
opacity: 1,
marginLeft: 0
}, ANIMATION_SPEED);
// Animate the list so we stay centered during the container getting smaller
$list
.stop()
.animate({left: calculateListLeft($previouslySelectedItem)}, ANIMATION_SPEED);
// Fade out the big detail image
$detail
.stop()
.animate(detailCss, ANIMATION_SPEED, scrollToSelected)
.removeClass('show');
$selectedItem = $item;
}
function scrollToSelected() {
state = 'scrollToSelected';
addInfiniteScrollItems();
if ($detail) {
// Remove opacity style from the previusly selected small image
// Otherwise potential :hover selectors can't overwrite it
$previouslySelectedItem
.find('img')
.css('opacity', '');
}
$list
.stop()
.animate({left: calculateListLeft($selectedItem)}, ANIMATION_SPEED, function() {
if (!$detail) {
// no detail image? we are done : )
return;
}
waitForDetailImage();
});
if ($spinner) {
$spinner.hide();
}
}
function waitForDetailImage() {
state = 'waitForDetailImage';
if (!$detail) {
// Nothing to do here for regular scrollables
animateSelected();
return;
}
// Set the detail image
var detailUrl = $selectedItem.dataset('detailurl') || $img.attr('src');
if ($.support.opacity) {
$detailImage.attr('src', detailUrl);
} else {
// This is needed for IE7, otherwise there is a black border around the transparent image
// See discussion here: http://www.optimalworks.net/blog/2008/web-development/ie-png-filter-problems
$detailImage.css('filter', 'filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+detailUrl+'", sizingMethod="scale")');
}
if ($spinner) {
$selectedItem.append($spinner);
$spinner.show();
}
var $wasSelectedItem = $selectedItem;
onLoad(detailUrl, function() {
// The user may select another image while he is waiting for the current one to load
// if that has happened, we need to ignore the callback from the new deselected image
if ($selectedItem !== $wasSelectedItem) {
return;
}
$spinner.remove();
animateSelected();
});
}
function animateSelected() {
state = 'animateSelected';
if (!$detail) {
// Not much todo, just add the selected class
$selectedItem.addClass('selected');
$selectedItem.trigger('selected');
return;
}
// Animate the list so the selected item will be centered when it is
// displayed as being selected.
$list.animate({left: calculateListLeft($selectedItem, true)}, ANIMATION_SPEED);
// Animate the selected item container
$selectedItem
.animate({width: selectedItemWidth}, ANIMATION_SPEED)
.addClass('selected');
// Animate the selected item's small image to fade out
$selectedItem
.find('img')
.animate({
opacity: 0,
marginLeft: (selectedItemWidth - itemWidth) / 2
}, ANIMATION_SPEED);
// Animate the detail container in
$detail
.animate(detailShowCss, ANIMATION_SPEED)
.addClass('show');
$selectedItem.trigger('selected');
}
direction = null;
if ($previouslySelectedItem) {
direction = 'left';
if ($item.prevAll().filter($previouslySelectedItem).length) {
direction = 'right';
}
}
if (!$previouslySelectedItem) {
$selectedItem = $item;
waitForDetailImage();
return;
}
animatePrevioslySelected();
}
init();
});
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment