(function ($){ /*! * mcDropdown jQuery Plug-in * * Copyright 2011 Giva, Inc. (http://www.givainc.com/labs/) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Date: 2011-07-25 * Rev: 1.3.1 */ $.fn.mcDropdown = function(list, options) { // track the dropdown object var dd; // create a dropdown for each match this.each(function() { dd = $.data(this, "mcDropdown"); // we're already a dropdown, return a reference to myself if( dd ) return false; new $.mcDropDownMenu(this, list, options); }); // return either the dropdown object or the jQuery object reference return dd || this; }; // set default options $.mcDropdown = { version: "1.3.1", setDefaults: function(options){ $.extend(defaults, options); } }; // set the defaults var defaults = { minRows: 8 // specify the minimum rows before creating a new column , maxRows: 25 // specify the maximum rows in a column , targetColumnSize: 2 // specify the default target column size (it'll attempt to create this many columns by default, unless the min/max row rules are not being met) , openFx: "slideDown" // the fx to use for showing the root menu , openSpeed: 250 // the speed of the openFx , closeFx: "slideUp" // the fx to use for hiding the root menu , closeSpeed: 250 // the speed of the closeFx , hoverOverDelay: 200 // the delay before opening a submenu , hoverOutDelay: 0 // the delay before closing a submenu , showFx: "show" // the fx to use when showing a submenu , showSpeed: 0 // the speed of the showFx , hideFx: "hide" // the fx to use when closing a submenu , hideSpeed: 0 // the speed of the hideFx , dropShadow: true // determine whether drop shadows should be shown on the submenus , autoHeight: true // always uses the lineHeight options (much faster than calculating height) , lineHeight: 19 // the base height of each list item (li) this is normally calculated automatically, but in some cases the value can't be determined and you'll need to set it manually , screenPadding: 10 // the padding to use around the border of the screen -- this is used to make sure items stay on the screen , allowParentSelect: false // determines if parent items are allowed to be selected (by default only end nodes can be selected) , delim: ":" // the delimited to use when showing the display string , showACOnEmptyFocus: false // show the autocomplete box on focus when input is empty , valueAttr: "rel" // the attribute that contains the value to use in the hidden field , click: null // callback that occurs when the user clicks on a menu item , select: null // callback that occurs when a value is selected , init: null // callback that occurs when the control is fully initialized }; // check to see if the browser is IE6 var isIE6 = ($.browser.version && $.browser.version <= 6); $.mcDropDownMenu = function(el, list, options){ var $self, thismenu = this, $list, $divInput, settings, typedText = "", matchesCache, oldCache, $keylist, $keylistiframe, bInput, bDisabled = false; // create a reference to the dropdown $self = $(el); // is the field and input element bInput = $self.is(":input"); // get the settings for this instance settings = $.extend({}, defaults, options); // set the default click behavior if( settings.click == null ) { settings.click = function (e, dropdown, settings){ if( this.attr(settings.valueAttr) ){ dropdown.setValue(this.attr(settings.valueAttr)); } else { dropdown.setValue($(this.parents("li")[0]).attr(settings.valueAttr)); } }; } // attach window behaviors $(document) // Bind a click event to hide all visible menus when the document is clicked .bind("click", function(e){ // get the target that was clicked var $target = $(e.target); var $ul = $target.parents().filter(function (){ return this === $list[0] || (!!$keylist && $keylist[0] === this); }); // check to make sure the clicked element was inside the list if( $ul.length ){ var bIsParent = $target.is(".mc_parent"); // if we've clicked a parent item in the autocomplete box, we must adjust the current value if( bIsParent && $keylist && $ul[0] === $keylist[0] ){ updateValue($target.find("> ul > li:first"), false); e.stopPropagation(); return false; } // check to see if the user can click on parent items else if( !settings.allowParentSelect && bIsParent ) return false; // make sure to hide the parent branch if we're not the root if( $target.not(".mc_root") ) hideBranch.apply($target.parent().parent()[0], [e]); if( settings.click != null && settings.click.apply($target, [e, thismenu, settings]) == false ){ return false; } } // close the menu thismenu.closeMenu(); }); // store a reference to the list, if it's not already a jQuery object make it one $list = (((typeof list == "object") && !!list.jquery)) ? list : $(list); // we need to calculate the visual width for each nested list $list // move list to body -- this allows us to always calculate the correct position & width of the elements .appendTo("body") // move the list way off screen .css({position: "absolute", top: -10000, left: -10000}) // find all the ul tags .find("ul") // add the root ul tag to the array .andSelf() // make all the nodes visible .css("display", "block") // loop through each node .each(function (){ var $el = $(this); // calculate the width of the element -- using clientWidth is 2x as fast as width() $el.data("width", $el[0].clientWidth); }) // now that we've gotten the widths, hide all the lists and move them to x:0, y:0 .css({top: 0, left: 0, display: "none"}); // mark the root children items $list.find("> li").addClass("mc_root"); // add parent class $("li > ul", $list).parent().addClass("mc_parent"); // create the div to wrap everything in $divInput = $('
') .appendTo($('')) .parent(); // get a reference to the input element and remove it from the DOM var $input = $self.replaceWith($divInput).attr({id: "", name: ""}); // get a reference to the hidden form field var $hidden = $divInput.find(":input"); // put the input element back in the div.mcdropdown layer $divInput = $divInput.find(".mcdropdown").prepend($input); // make a visible copy of the element so we can get the correct sizes, then delete it var $divInputClone = $divInput.clone().css({position: "absolute", top: -9999999, left: -999999, visibility: "visible"}).show().appendTo("body"); var di = {width: $divInputClone.width() - $("a", $divInputClone).width(), height: $divInputClone.outerHeight()} $divInputClone.remove(); // store a reference to this link select $.data($hidden[0], "mcDropdown", thismenu); // update the height of the outer relative div, this allows us to // correctly anchor the dropdown $divInput.parent().height(di.height); // safari will not get the correct width until after everything has rendered if( $.browser.safari ){ setTimeout(function (){ $self .width($divInput.width() - $("a", $divInput).width()); }, 100); } // adjust the width of the new input element $self .width(di.width) // make sure we only attach the next events if we're in input element .filter(":input") // turn autocomplete off .attr("autocomplete", "off") // add key stroke bindings (IE6 requires keydown) .bind("keypress", checkKeypress) // prevent user from selecting text .bind("mousedown", function (e){ $(this).triggerHandler("focus"); e.stopPropagation(); return false; }) // disable context menu .bind("contextmenu", function (){ return false; }) // select the text when the cursor is placed in the field .bind("focus", onFocus) // when the user leaves the text field .bind("blur", onBlur); // IE6 doesn't register certain keypress events, so we must catch them during the keydown event if( $.browser.msie || $.browser.safari) $self.bind("keydown", function (e){ // check to see if a key was pressed that IE6 doesn't trigger a keypress event for if( ",8,9,37,38,39,40,".indexOf("," + e.keyCode + ",") > -1 ) return checkKeypress(e); }); // attach a click event to the anchor $("a", $divInput).bind("click", function (e){ // if disabled, skip processing if( bDisabled ) return false; thismenu.openMenu(e); return false; }); // set the value of the field this.setValue = function (value, skipCallback){ // update the hidden value $hidden.val(value); // get the display name var name = displayString(value); // run the select callback (some keyboard entry methods will manage this callback manually) if( settings.select != null && skipCallback != true ) settings.select.apply(thismenu, [value, name]); // update the display value and return the jQuery object return $self[bInput ? "val" : "text"](name); }; // set the default value (but don't run callback) if( bInput ) this.setValue($self[0].defaultValue, true); // get the value of the field (returns array) this.getValue = function (value){ return [$hidden.val(), $self[bInput ? "val" : "text"]()]; }; // open the menu programmatically this.openMenu = function (e){ // if the menu is open, kill processing if( $list.is(":visible") ){ // on a mouse click, close the menu, otherwise just cancel return (!!e) ? thismenu.closeMenu() : false; } function open(){ // columnize the root list columnizeList($list).hide(); // add the bindings to the menu addBindings($list); // anchor the menu relative parent anchorTo($divInput.parent(), $list, true); // remove existing hover classes, which might exist from keyboard entry $list.find(".mc_hover").removeClass("mc_hover"); // show the menu $list[settings.openFx](settings.openSpeed, function (){ // scroll the list into view scrollToView($list); }); // if the bgIframe exists, use the plug-in if( isIE6 && !!$.fn.bgIframe ) $list.bgIframe(); } // if this is triggered via an event, just open the menu if( e ) open(); // otherwise we need to open the menu asynchronously to avoid collision with $(document).click event else setTimeout(open, 1); }; // close the menu programmatically this.closeMenu = function (e){ // hide any open menus $list.find("ul:visible").parent().each(function (){ hideBranch.apply(this); }); // remove the bindings removeBindings($list); // close the menu $list[settings.closeFx](settings.closeSpeed); }; // place focus in the input box this.focus = function (){ $self.focus(); }; // disable the element this.disable = function (status){ // change the disabled status bDisabled = !!status; $divInput[bDisabled ? "addClass" : "removeClass"]("mcdropdownDisabled"); $input.attr("disabled", bDisabled); }; function getNodeText($el){ var nodeContent; var nContents = $el.contents().filter(function() { // remove empty text nodes and comments return (this.nodeType == 1) || (this.nodeType == 3 && $.trim(this.nodeValue).length>0); }); // return either an empty string or the node's value if (nContents[0] && nContents[0].nodeType == 3) { // Text node : take it's value nodeContent = nContents[0].nodeValue; } else if (nContents[0] && nContents[0].nodeType == 1) { // Element node : take the contents nodeContent = $(nContents[0]).text(); } else { nodeContent = ""; } return $.trim(nodeContent); }; function getTreePath($li){ if( $li.length == 0 ) return []; var name = [getNodeText($li)]; // loop through the parents and get the value $li.parents().each(function (){ var $el = $(this); // break when we get to the main list element if( this === $list[0] ) return false; else if( $el.is("li") ) name.push(getNodeText($el)); }); // return the display name return name.reverse(); }; function displayValue(value){ // return the path as an array return getTreePath(getListItem(value)); }; function displayString(value){ // return the display name return displayValue(value).join(settings.delim); }; function parseTree($selector){ var s = [], level = (arguments.length > 1) ? ++arguments[1] : 1; // loop through all the children and store information about the tree $("> li", $selector).each( function (){ // get a reference to the current object var $self = $(this); // look for a ul tag as a direct child var $ul = $("> ul", this); // push a reference to the element to the tree array s.push({ // get the name of the node name: getNodeText($self) // store a reference to the current element , element: this // parse and store any children items , children: ($ul.length) ? parseTree($ul, level) : [] }); } ); return s; }; function addBindings(el){ removeBindings(el); $("> li", el) .bind("mouseover", hoverOver) .bind("mouseout", hoverOut); }; function removeBindings(el){ $("> li", el) .unbind("mouseover", hoverOver) .unbind("mouseout", hoverOut); }; // scroll the current element into view function scrollToView($el){ // get the current position var p = position($el, true); // get the screen dimensions var sd = getScreenDimensions(); // if we're hidden off the bottom of the page, move up if( p.bottom > sd.y ){ $("html,body").animate({"scrollTop": "+=" + ((p.bottom - sd.y) + settings.screenPadding) + "px" }) } }; function hoverOver(e){ var self = this; var timer = $.data(self, "timer"); // if the timer exists, clear it if( !isNaN(timer) ) clearTimeout(timer); // if IE6, add the hover class $(this).addClass("mc_hover"); // show the branch $.data(self, "timer", setTimeout(function(){ showBranch.apply(self); }, settings.hoverOverDelay) ); }; function hoverOut(e){ var self = this; var timer = $.data(self, "timer"); // if the timer exists, clear it if( !isNaN(timer) ) clearTimeout(timer); // if IE6, remove the hover class $(this).removeClass("mc_hover"); // hide the branch $.data(self, "timer", setTimeout(function(){ var $li = $(self); setTimeout(function (){ // if no children selected, we must close the parent menus if( $li.parent().find("> li.mc_hover").length == 0 ){ $li.parents("li").each(function (){ var self = this; clearTimeout($.data(self, "timer")); hideBranch.apply(self); // check to see if we've hovered over a parent item if( $(this).siblings().filter(".mc_hover").length > 0 ) return false; }); } }, settings.hoverOverDelay); hideBranch.apply(self); }, settings.hoverOutDelay) ); // this will stop flickering in IE6, but it leaves mc_hover classes behind if( isIE6 ) e.stopPropagation(); }; function getShadow(depth){ var shadows = $self.data("shadows"); // if the shadows don't exist, create an object to track them if( !shadows ) shadows = {}; // if the shadow doesn't exist, create it if( !shadows[depth] ){ // create shadow shadows[depth] = $('').appendTo('body'); // if the bgIframe exists, use the plug-in if( !!$.fn.bgIframe ) shadows[depth].bgIframe(); // update the shadows cache $self.data("shadows", shadows); } return shadows[depth]; }; function showBranch(){ var self = this; // the child menu var $ul = $("> ul", this); // if the menu is already visible or there is no submenu, cancel if( $ul.is(":visible") || ($ul.length == 0) ) return false; // hide any visible sibling menus $(this).parent().find('> li ul:visible').not($ul).parent().each(function(){ hideBranch.apply(this); }); // columnize the list columnizeList($ul); // add new bindings addBindings($ul); var depth = $ul.parents("ul").length; // get the screen dimensions var sd = getScreenDimensions(); // get the coordinates for the menu item var li_coords = position($(this)); // move the menu to the correct position and show the menu || ((depth)*2) $ul.css({top: li_coords.bottom, left: li_coords.marginLeft/*, zIndex: settings.baseZIndex + ((depth)*2)*/}).show(); // get the bottom of the menu var menuBottom = $ul.outerHeight() + $ul.offset().top; // if we're hidden off the bottom of the page, move up if( menuBottom > sd.y ){ // adjust the menu by subtracting the bottom edge by the screen offset $ul.css("top", li_coords.bottom - (menuBottom - sd.y) - settings.screenPadding); } var showShadow = function (){ // if using drop shadows, then show them if( settings.dropShadow ){ // get a reference to the current shadow var $shadow = getShadow(depth); // get the position of the parent element var pos = position($ul); // move the shadow to the correct visual & DOM position $shadow.css({ top: pos.top + pos.marginTop , left: pos.left + pos.marginLeft , width: pos.width , height: pos.height /*, zIndex: settings.baseZIndex + ((2*depth)-1)*/ }).insertAfter($ul).show(); // store a reference to the shadow so we can hide it $.data(self, "shadow", $shadow); } } // columnize the list and then show it using the defined effect // if the menu has a zero delay, just open it and then draw the // shadow, otherwise show the effect and the draw the shadow // after you're done. if( settings.showSpeed <= 0 ){ showShadow(); } else { $ul.hide()[settings.showFx](settings.showSpeed, showShadow); } }; function hideBranch(){ var $ul = $("> ul", this); // if the menu is already visible or there is no submenu, cancel if( $ul.is(":hidden") || ($ul.length == 0) ) return false; // if using drop shadows, then hide if( settings.dropShadow && $.data(this, "shadow") ) $.data(this, "shadow").hide(); // if we're IE6, we need to set the visiblity to "hidden" so child // menus are correctly hidden and remove the .mc_hover class due to // the e.stopPropagation() call in the hoverOut() call if( isIE6 ) $ul.css("visibility", "hidden").parent().removeClass("mc_hover"); // hide the menu $ul.stop()[settings.hideFx](settings.hideSpeed); }; function position($el, bUseOffset){ var bHidden = false; // if the element is hidden we must make it visible to the DOM to get if ($el.is(":hidden")) { bHidden = !!$el.css("visibility", "hidden").show(); } var pos = $.extend($el[bUseOffset === true ? "offset" : "position"](),{ width: $el.outerWidth() , height: $el.outerHeight() , marginLeft: parseInt($.curCSS($el[0], "marginLeft", true), 10) || 0 , marginRight: parseInt($.curCSS($el[0], "marginRight", true), 10) || 0 , marginTop: parseInt($.curCSS($el[0], "marginTop", true), 10) || 0 , marginBottom: parseInt($.curCSS($el[0], "marginBottom", true), 10) || 0 }); if( pos.marginTop < 0 ) pos.top += pos.marginTop; if( pos.marginLeft < 0 ) pos.left += pos.marginLeft; pos["bottom"] = pos.top + pos.height; pos["right"] = pos.left + pos.width; // hide the element again if( bHidden ) $el.hide().css("visibility", "visible"); return pos; }; function anchorTo($anchor, $target, bUseOffset){ var pos = position($anchor, bUseOffset); $target.css({ position: "absolute" , top: pos.bottom , left: pos.left }); /* * we need to return the top edge of the core drop down menu, because * the top:0 starts at this point when repositioning items absolutely * this means we have to offset everything by the offset of the top menu */ return pos.bottom; }; function getScreenDimensions(){ var d = { scrollLeft: $(window).scrollLeft() , scrollTop: $(window).scrollTop() , width: $(window).width() // changed from innerWidth , height: $(window).height() // changed from innerHeight }; // calculate the correct x/y positions d.x = d.scrollLeft + d.width; d.y = d.scrollTop + d.height; return d; }; function getPadding(el, name){ var torl = name == 'height' ? 'Top' : 'Left', // top or left borr = name == 'height' ? 'Bottom' : 'Right'; // bottom or right return ( // we add "0" to each string to make sure parseInt() returns a number parseInt("0"+$.curCSS(el, "border"+torl+"Width", true), 10) + parseInt("0"+$.curCSS(el, "border"+borr+"Width", true), 10) + parseInt("0"+$.curCSS(el, "padding"+torl, true), 10) + parseInt("0"+$.curCSS(el, "padding"+borr, true), 10) + parseInt("0"+$.curCSS(el, "margin"+torl, true), 10) + parseInt("0"+$.curCSS(el, "margin"+borr, true), 10) ); }; function getListDimensions($el, cols){ if( !$el.data("dimensions") ){ // get the width of the dropdown menu var ddWidth = $divInput.outerWidth(); // if showing the root item, then try to make sure the width of the menu is sized to the drop down menu var width = ( ($el === $list) && ($el.data("width") * cols < ddWidth) ) ? Math.floor(ddWidth/cols) : $el.data("width"); $el.data("dimensions", { // get the original width of the list item column: width // subtract the padding from the first list item from the width to get the width of the items , item: width - getPadding($el.children().eq(0)[0], "width") // get the original height , height: $el.height() }); } return $el.data("dimensions"); }; function getHeight($el){ // skip height calculation and use lineHeight if( settings.autoHeight === false ) return settings.lineHeight; // if we haven't cached our height, do so now if( !$el.data("height") ) $el.data("height", $el.outerHeight()); // return the cached value return $el.data("height"); }; function columnizeList($el){ // get the children items var $children = $el.find("> li"); // get the total number of items var items = $children.length; // calculate how many columns we think we should have based on the max rows var calculatedCols = Math.ceil(items/settings.maxRows); // get the number of columns, don't columnize if we don't have enough rows // if the height of the column is bigger than the screen, we automatically try // moving to a new column var cols = !!arguments[1] ? arguments[1] : ( items <= settings.minRows ) ? 1 : (calculatedCols > settings.targetColumnSize) ? calculatedCols : settings.targetColumnSize; // get the dimension of this element var widths = getListDimensions($el, cols); var prevColumn = 0; var columnHeight = 0; var maxColumnHeight = 0; var maxRows = Math.ceil(items/cols); // get the width of the parent item var parentLIWidth = $el.parent("li").width(); // we need to draw the list element, but hide it so we can correctly calculate it's information $el.css({"visibility": "hidden", "display": "block"}); // loop through each child item $children.each(function (i){ var currentItem = i+1; var nextItemColumn = Math.floor((currentItem/items) * cols); // calculate the column we're in var column = Math.floor((i/items) * cols); // reference the current item var $li = $(this); // variable to track margin-top var marginTop; // if we're in the same column if( prevColumn != column ){ // move to the top of the next column marginTop = (columnHeight+1) * -1; // reset column height columnHeight = 0; // if we're in a new column } else { marginTop = 0; } // increase the column height based on it's current height (calculate this before adding classes) columnHeight += (getHeight($li) || settings.lineHeight); // update the css settings $li.css({ "marginLeft": (widths.column * column) , "marginTop": marginTop , "width": widths.item }) [((nextItemColumn > column) || (currentItem == items)) ? "addClass" : "removeClass"]("mc_endcol") [(marginTop != 0) ? "addClass" : "removeClass"]("mc_firstrow") ; // get the height of the longest column if( columnHeight > maxColumnHeight ) maxColumnHeight = columnHeight; // update the previous column prevColumn = column; }); // if the menu is too tall to fit on the screen, try adding another column if( ($el !== $list) && (maxColumnHeight + (settings.screenPadding*2) >= getScreenDimensions().height) ){ return columnizeList($el, cols+1); } /* * set the height of the list to the max column height. this fixes * display problems in FF when the last column is not full. * * we also need to set the visiblity to "visible" to make sure that * the element will show up */ $el.css("visibility", "visible").height(maxColumnHeight); return $el; }; function getListItem(value){ return $list.find("li[" + settings.valueAttr + "='"+ value +"']"); }; function getCurrentListItem(){ return getListItem($hidden.val()); }; function onFocus(e){ var $current = getCurrentListItem(); var value = $self.val().toLowerCase(); var treePath = value.toLowerCase().split(settings.delim); var currentNode = treePath.pop(); var lastDelim = value.lastIndexOf(settings.delim) + 1; // reset the typed text typedText = treePath.join(settings.delim) + (treePath.length > 0 ? settings.delim : ""); // we need to set the selection asynchronously so that when user TABs to field the pre-select isn't overwritten setTimeout(function (){ // preselect the last child node setSelection($self[0], lastDelim, lastDelim+currentNode.length); }, 0); // create the keyboard hint list if( !$keylist ){ $keylist = $('