/*
jQuery-SelectBox
Traditional select elements are very difficult to style by themselves,
but they are also very usable and feature rich. This plugin attempts to
recreate all selectbox functionality and appearance while adding
animation and stylability.
This product includes software developed
by RevSystems, Inc (http://www.revsystems.com/) and its contributors
Please see the accompanying LICENSE.txt for licensing information.
*/
(function($) {
jQuery.fn.borderWidth = function() { return $(this).outerWidth() - $(this).innerWidth(); };
jQuery.fn.marginWidth = function() { return $(this).outerWidth(true) - $(this).outerWidth(); };
jQuery.fn.paddingWidth = function() { return $(this).innerWidth() - $(this).width(); };
jQuery.fn.extraWidth = function() { return $(this).outerWidth(true) - $(this).width(); };
jQuery.fn.offsetFrom = function($e) {
return {
left: $(this).offset().left - $e.offset().left,
top: $(this).offset().top - $e.offset().top
};
};
jQuery.fn.maxWidth = function() {
var max = 0;
$(this).each(function() {
if($(this).width() > max) max = $(this).width();
});
return max;
}
jQuery.fn.sb = function(o) {
if($.browser.msie && $.browser.version < 7) return $(this);
o = $.extend({
acTimeout: 800, // time between each keyup for the user to create a search string
animDuration: 300, // time to open/close dropdown in ms
ddCtx: 'body', // body | self | any selector | a function that returns a selector (the original select is the context)
dropupThreshold: 150, // the minimum amount of extra space required above the selectbox for it to display a dropup
fixedWidth: true, // if false, dropdown expands to widest and display conforms to whatever is selected
maxHeight: false, // if an integer, show scrollbars if the dropdown is too tall
maxWidth: false, // if an integer, prevent the display/dropdown from growing past this width; longer items will be clipped
noScrollThreshold: 100, // the minimum height of the dropdown before it can show scrollbars--very rarely applied
selectboxClass: 'selectbox', // class to apply our markup
useTie: false, // if jquery.tie is included and this is true, the selectbox will update dynamically
// markup appended to the display, typically for styling an arrow
arrowMarkup: "",
// formatting for the display; note that it will be wrapped with
optionFormat: function(ogIndex, optIndex) {
return $(this).text();
},
// the function to produce optgroup markup
optgroupFormat: function(ogIndex) {
return "" + $(this).attr("label") + "";
}
}, o);
$(this).each(function() {
var $orig = $(this);
var $sb = null;
var $display = null;
var $dd = null;
var $items = null;
function loadSB() {
// create the new markup from the old
$sb = $("
");
$("body").append($sb);
$display = $("" + $orig.val() + " " + o.optionFormat.call($orig.find("option:selected")[0], 0, 0) + "" + o.arrowMarkup + "");
$sb.append($display);
$dd = $("");
$sb.append($dd);
$orig.children().each(function(i) {
if($(this).is("optgroup")) {
var $og = $(this);
var $ogItem = $("" + o.optgroupFormat.call($og[0], i+1) + "");
var $ogList = $("");
$ogItem.append($ogList);
$dd.append($ogItem);
$og.children("option").each(function(j) {
var $li = $("" + $(this).attr("value") + "" + o.optionFormat.call(this, i+1, j+1) + "");
$li.data("val", $(this).attr("value"));
$ogList.append($li);
});
}
else {
var $li = $("" + $(this).attr("value") + "" + o.optionFormat.call(this, 0, i+1) + "")
$li.data("val", $(this).attr("value"));
$dd.append($li);
}
});
$items = $dd.find("li").not(".optgroup");
$dd.children(":first").addClass("first");
$dd.children(":last").addClass("last");
$orig.hide();
if(o.fixedWidth) {
// match display size to largest element
var largestWidth = $sb.find(".text, .optgroup").maxWidth() + $display.extraWidth() + 1;
$sb.width(o.maxWidth ? Math.min(o.maxWidth, largestWidth) : largestWidth);
if($.browser.msie && $.browser.version <= 7) {
$items.find("a").each(function() {
$(this).css("width", "100%").width($(this).width() - $(this).paddingWidth() - $(this).borderWidth());
});
}
}
else if(o.maxWidth && $sb.width() > o.maxWidth) {
$sb.width(o.maxWidth);
}
$orig.before($sb);
// initialize dd and bindings
$dd.hide();
if(!$orig.is(":disabled")) {
$display.click(clickSB).focus(focusSB).blur(blurSB).hover(addHoverState, removeHoverState);
$items.not(".disabled").find("a").click(clickSBItem);
$items.filter(".disabled").find("a").click(function() { return false; });
$items.not(".disabled").hover(addHoverState, removeHoverState);
$dd.find(".optgroup").hover(addHoverState, removeHoverState).click(function() { return false; });
}
else {
$sb.addClass("disabled");
$display.click(function(e) { e.preventDefault(); });
}
$sb.bind("close", closeSB);
$sb.bind("destroy", destroySB);
$orig.bind("reload", reloadSB);
if(jQuery.fn.tie && o.useTie) {
$orig.bind("domupdate", delayReloadSB);
}
$orig.focus(focusOrig);
}
function focusOrig() { $display.focus(); return false; }
var delayReloadTimeout = null;
function delayReloadSB() {
clearTimeout(delayReloadTimeout);
delayReloadTimeout = setTimeout(reloadSB, 30);
}
function reloadSB() {
var isOpen = $sb.is(".open");
var isFocused = $display.is(".focused");
instantCloseSB();
destroySB();
loadSB();
if(isOpen) {
$display.focus();
instantOpenSB();
}
else if(isFocused) {
$display.focus();
}
}
// unbind and remove
function destroySB() {
$sb.unbind().find("*").unbind();
$sb.remove();
$orig.unbind("reload", reloadSB).unbind("domupdate", delayReloadSB).unbind("focus", focusOrig).show();
}
// when the user clicks outside the sb
function killAndUnbind() {
killAll();
$(document).unbind("click", killAndUnbind);
}
// trigger all sbs to close
function killAll() {
$("." + o.selectboxClass).trigger("close");
}
// to prevent multiple selects open at once
function killAllButMe() {
$("." + o.selectboxClass).not($sb[0]).trigger("close");
}
// hide and reset dropdown markup
function closeSB() {
$items.removeClass("hover");
$(document).unbind("keyup", keyupSB);
$(document).unbind("keydown", stopPageHotkeys);
$(document).unbind("keydown", keydownSB);
$dd.fadeOut(o.animDuration, function() {
$sb.removeClass("open");
$sb.append($dd);
});
}
function instantCloseSB() {
$items.removeClass("hover");
$(document).unbind("keyup", keyupSB);
$(document).unbind("keydown", stopPageHotkeys);
$dd.hide();
$sb.removeClass("open");
$sb.append($dd);
}
function getDDCtx() {
var $ddCtx = null;
if(o.ddCtx == "self") {
$ddCtx = $sb;
}
else if($.isFunction(o.ddCtx)) {
$ddCtx = $(o.ddCtx.call($orig[0]));
}
else {
$ddCtx = $(o.ddCtx);
}
return $ddCtx;
}
function centerOnSelected() {
$dd.scrollTop($dd.scrollTop() + $items.filter(".selected").offsetFrom($dd).top - $dd.height() / 2 + $items.filter(".selected").outerHeight(true) / 2);
}
// show, reposition, and reset dropdown markup
function openSB() {
var $ddCtx = getDDCtx();
killAll();
$sb.addClass("open");
var dir = positionSB();
$ddCtx.append($dd);
if($.browser.msie && $.browser.version < 8) {
// fix ie7 display bug
$("." + o.selectboxClass + " .display").hide().show();
}
if(dir == "up") $dd.fadeIn(o.animDuration, centerOnSelected);
else if(dir == "down") $dd.slideDown(o.animDuration, centerOnSelected);
else $dd.fadeIn(o.animDuration, centerOnSelected);
$(document).click(killAndUnbind);
$display.focus();
}
function instantOpenSB() {
var $ddCtx = getDDCtx();
killAll();
$sb.addClass("open");
var dir = positionSB();
$ddCtx.append($dd);
if($.browser.msie && $.browser.version < 8) {
// fix ie7 display bug
$("." + o.selectboxClass + " .display").hide().show();
}
$dd.show();
centerOnSelected();
$(document).click(killAndUnbind);
$display.focus();
}
// position dropdown based on collision detection
function positionSB() {
var $ddCtx = getDDCtx();
var ddMaxHeight = 0;
var ddY = 0;
var dir = "";
// modify dropdown css for getting values
$dd.removeClass("above");
$dd.css({
display: "block",
maxHeight: "none",
position: "relative",
visibility: "hidden"
});
if(o.fixedWidth) $dd.width($display.outerWidth() - $dd.extraWidth() + 1);
// figure out if we should show above/below the display box
var bottomSpace = $(window).scrollTop() + $(window).height() - $display.offset().top - $display.outerHeight();
var topSpace = $display.offset().top - $(window).scrollTop();
var bottomOffset = $display.offsetFrom($ddCtx).top + $display.outerHeight();
var spaceDiff = bottomSpace - topSpace + o.dropupThreshold;
if($dd.outerHeight() < bottomSpace) {
ddMaxHeight = o.maxHeight ? o.maxHeight : bottomSpace;
ddY = bottomOffset;
dir = "down";
}
else if($dd.outerHeight() < topSpace) {
ddMaxHeight = o.maxHeight ? o.maxHeight : topSpace;
ddY = $display.offsetFrom($ddCtx).top - Math.min(ddMaxHeight, $dd.outerHeight());
dir = "up";
}
else if(spaceDiff >= 0) {
ddMaxHeight = o.maxHeight ? o.maxHeight : bottomSpace;
ddY = bottomOffset;
dir = "down";
}
else if(spaceDiff < 0) {
ddMaxHeight = o.maxHeight ? o.maxHeight : topSpace;
ddY = $display.offsetFrom($ddCtx).top - Math.min(ddMaxHeight, $dd.outerHeight());
dir = "up";
}
else {
ddMaxHeight = o.maxHeight ? o.maxHeight : "none";
ddY = bottomOffset;
dir = "down";
}
// modify dropdown css for display
var bodyX = $().jquery < "1.4.2" ? $("body").offset().left : parseInt($("body").css("margin-left"));
var bodyY = $().jquery < "1.4.2" ? $("body").offset().top : parseInt($("body").css("margin-top"));
$dd.css({
display: "none",
left: $display.offsetFrom($ddCtx).left + ($ddCtx[0].tagName.toLowerCase() == "body" ? bodyX : 0),
maxHeight: ddMaxHeight,
position: "absolute",
top: ddY + ($ddCtx[0].tagName.toLowerCase() == "body" ? bodyY : 0),
visibility: "visible"
});
if(dir == "up") $dd.addClass("above");
return dir;
}
// when the user explicitly clicks the display
function clickSB(e) {
var $sb = $(this).closest("." + o.selectboxClass);
if($sb.is(".open")) {
closeSB();
}
else {
$display.focus();
openSB();
}
return false;
}
// when the user selects an item in any manner
function selectItem() {
var $item = $(this);
$display.find(".value").html($item.find(".value").html());
$display.find(".text").html($item.find(".text").html());
$display.find(".text").attr("title", $item.find(".text").html());
$dd.find("li").removeClass("selected");
$item.closest("li").addClass("selected");
var oldVal = $orig.val();
var newVal = $item.closest("li").data("val");
$orig.val(newVal);
if(oldVal != newVal) {
$orig.change();
}
}
// when the user explicitly clicks an item
function clickSBItem(e) {
selectItem.call(this);
killAndUnbind();
$display.focus();
return false;
}
// helper functions for matching on keyup
var searchTerm = "";
var cstTimeout = null;
function clearSearchTerm() {
searchTerm = "";
}
function findMatchingItem(term) {
var ts = "";
var $available = $items.not(".disabled");
for(var i=0; i < $available.size(); i++) {
var t = $available.eq(i).find(".text").text();
ts += t + " ";
if(t.toLowerCase().match("^" + term.toLowerCase()) == term.toLowerCase()) {
return $available.eq(i);
}
}
return null;
}
function selectMatchingItem(text) {
var $matchingItem = findMatchingItem(text);
if($matchingItem != null) {
selectItem.call($matchingItem[0]);
return true;
}
return false;
}
function stopPageHotkeys(e) {
// Stop up/down/backspace/space from moving the page
if(e.which == 38 || e.which == 40 || e.which == 8 || e.which == 32) {
e.preventDefault();
}
}
// go up/down using arrows or attempt to autocomplete based on string
function keydownSB(e) {
if(e.altKey || e.ctrlKey) return false;
var $selected = $items.filter(".selected");
switch(e.which) {
case 35: // end
if($selected.size() > 0) {
e.preventDefault();
selectItem.call($items.not(".disabled").filter(":last")[0]);
centerOnSelected();
}
break;
case 36: // home
if($selected.size() > 0) {
e.preventDefault();
selectItem.call($items.not(".disabled").filter(":first")[0]);
centerOnSelected();
}
break;
case 38: // up
if($selected.size() > 0) {
if($items.not(".disabled").filter(":first")[0] != $selected[0]) {
e.preventDefault();
selectItem.call($items.not(".disabled").eq($items.not(".disabled").index($selected)-1)[0]);
}
centerOnSelected();
}
break;
case 40: // down
if($selected.size() > 0) {
if($items.not(".disabled").filter(":last")[0] != $selected[0]) {
e.preventDefault();
selectItem.call($items.not(".disabled").eq($items.not(".disabled").index($selected)+1)[0]);
centerOnSelected();
}
}
else if($items.size() > 1) {
e.preventDefault();
selectItem.call($items.eq(0)[0]);
}
break;
default:
break;
}
}
function keyupSB(e) {
if(e.altKey || e.ctrlKey) return false;
var $selected = $items.filter(".selected");
if(e.which != 38 && e.which != 40) {
searchTerm += String.fromCharCode(e.keyCode);
if(!selectMatchingItem(searchTerm)) {
clearTimeout(cstTimeout);
clearSearchTerm();
}
else {
clearTimeout(cstTimeout);
cstTimeout = setTimeout(clearSearchTerm, o.acTimeout);
}
}
}
// when the sb is focused (by tab or click), allow hotkey selection and kill all other selectboxes
function focusSB() {
killAllButMe();
$sb.addClass("focused");
$(document).unbind("keyup", keyupSB).keyup(keyupSB);
$(document).unbind("keydown", stopPageHotkeys).keydown(stopPageHotkeys);
$(document).unbind("keydown", keydownSB).keydown(keydownSB);
}
// when the sb is blurred (by tab or click), disable hotkey selection
function blurSB() {
$sb.removeClass("focused");
$(document).unbind("keyup", keyupSB);
$(document).unbind("keydown", stopPageHotkeys);
$(document).unbind("keydown", keydownSB);
}
function addHoverState() { $(this).addClass("hover"); }
function removeHoverState() { $(this).removeClass("hover"); }
loadSB();
});
};
})(jQuery);