From e67f4e7b19d3c78b07ca26662e83c506b2c2cfa4 Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Tue, 26 Jul 2011 17:20:34 +0200 Subject: try mcdropdown instead --- src/main/webapp/js/jquery.mcdropdown.js | 1139 +++++++++++++++++++++++++++++++ 1 file changed, 1139 insertions(+) create mode 100644 src/main/webapp/js/jquery.mcdropdown.js (limited to 'src/main/webapp/js/jquery.mcdropdown.js') diff --git a/src/main/webapp/js/jquery.mcdropdown.js b/src/main/webapp/js/jquery.mcdropdown.js new file mode 100644 index 0000000..a99c66b --- /dev/null +++ b/src/main/webapp/js/jquery.mcdropdown.js @@ -0,0 +1,1139 @@ +(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 = $('').appendTo("body"); + // if IE6 we need an iframe to hide the scrolling list + if( isIE6 && !!$.fn.bgIframe ) $keylistiframe = $('
').bgIframe().appendTo("body"); + } + + // should we show matches? + var hideResults = !(settings.showACOnEmptyFocus && (typedText.length == 0)); + + // get the siblings for the current item + var $siblings = ($current.length == 0 || $current.hasClass("mc_root")) ? $list.find("> li") : $current.parent().find("> li"); + // show all matches + showMatches($siblings, hideResults); + }; + + var iBlurTimeout = null; + function onBlur(e){ + // only run the last blur event + if( iBlurTimeout ) clearTimeout(iBlurTimeout); + // we may need to cancel this blur event, so we run it asynchronously + iBlurTimeout = setTimeout(function (){ + // get the current item + var $current = getCurrentListItem(); + + // if we must select a child item, then update to the first child we can find + if( !settings.allowParentSelect && $current.is(".mc_parent") ){ + // grab the first end child item we can find for the current path + var value = $current.find("li:not('.mc_parent'):first").attr(settings.valueAttr); + // update the value + thismenu.setValue(value, true); + } + + // run the select callback + if( settings.select != null ) settings.select.apply(thismenu, thismenu.getValue()); + + // hide matches + hideMatches(); + + // mark event as having run + iBlurTimeout = null; + }, 200); + }; + + function showMatches($li, hideResults){ + var bCached = ($li === oldCache), $items = bCached ? $keylist.find("> li").removeClass("mc_hover mc_hover_parent mc_firstrow") : $li.clone().removeAttr("style").removeClass("mc_hover mc_hover_parent mc_firstrow mc_endcol").filter(":last").addClass("mc_endcol").end(); + + // only do the following if we've updated the cache or the list is hidden + if( !bCached || $keylist.is(":hidden") ){ + // update the matches + $keylist.empty().append($items).width($divInput.outerWidth() - getPadding($keylist[0], "width")).css("height", "auto"); + + // anchor the menu relative parent + anchorTo($divInput.parent(), $keylist, true); + + // show hover on mouseover + $items.hover(function (){$keylist.find("> li").removeClass("mc_hover_parent mc_hover"); $(this).addClass("mc_hover")}, function (){$(this).removeClass("mc_hover")}); + + // make sure the the ul's are hidden (so the li's are sized correctly) + $items.find("> ul").css("display", "none"); + + // show the list + $keylist.show().css("visibility", (hideResults === true) ? "hidden" : "visible"); + + // if we're IE6, ensure we enforce the "max-height" CSS property + if( isIE6 ){ + var maxHeight = parseInt($keylist.css("max-height"), 10) || 0; + if( (maxHeight > 0) && (maxHeight < $keylist.outerHeight()) ) $keylist.height(maxHeight); + + // anchor the iframe behind the scrollable list + if( !!$.fn.bgIframe ) anchorTo($divInput.parent(), $keylistiframe.css({height: $keylist.outerHeight(), width: $keylist.width()}, true).show()) + } + + // scroll the list into view + if( hideResults !== true ) scrollToView($keylist); + } + + // do not show the list on screen + if( hideResults === true ){ + // hide the results and move them offscreen (so it doesn't hide the cursor in FF2) + $keylist.css({top: "-10000px", left: "-10000px"}); + // hiden the iframe overlay + if( isIE6 && !!$.fn.bgIframe ) $keylistiframe.css("display", "none"); + } + + // get the currently selected item + var $current = $keylist.find("li[" + settings.valueAttr + "='"+ $hidden.val() +"']"); + + // make sure the last match is still highlighted + $current.addClass("mc_hover" + ($current.is(".mc_parent")? "_parent" : "")); + + // scroll the item into view + if( $current.length > 0 && (hideResults != true) ) scrollIntoView($current); + + // update the cache + oldCache = matchesCache = $li; + }; + + function hideMatches(){ + // hide the bgiframe + if( isIE6 && !!$.fn.bgIframe && $keylistiframe ) $keylistiframe.hide(); + if( $keylist ) $keylist.hide(); + }; + + // check the user's keypress + function checkKeypress(e){ + var key = String.fromCharCode(e.keyCode || e.charCode).toLowerCase(); + var $current = getCurrentListItem(); + var $lis = ($current.length == 0 || $current.hasClass("mc_root")) ? $list.find("> li") : $current.parent().find("> li"); + var treePath = typedText.split(settings.delim); + var currentNode = treePath.pop(); + var compare = currentNode + key; + var selectedText = getSelection($self[0]).toLowerCase(); + var value = $self.val().toLowerCase(); + + // if the up arrow was pressed + if( e.keyCode == 38 ){ + moveMatch(-1); + return false; + + // if the down arrow was pressed + } else if( e.keyCode == 40 ){ + moveMatch(1); + return false; + + // if the [ESC] was pressed + } else if( e.keyCode == 27 ){ + // clear typedText + typedText = ""; + // clear the value + thismenu.setValue(""); + // show the root level + showMatches($list.find("> li")); + + return false; + + // if user pressed [DEL] or [LEFT ARROW], go remove last typed character + } else if( e.keyCode == 8 || e.keyCode == 37 ){ + // if left arrow, go back to previous parent + compare = (e.keyCode == 37) ? "" : currentNode.substring(0, currentNode.length - 1); + + // if all the text is highlighted we just came from a delete + if( selectedText == currentNode ){ + currentNode = ""; + } + // we're going backwards to the last parent, move backwards + if( treePath.length > 0 && currentNode.length == 0){ + updateValue($current.parent().parent()); + return false; + // if all the text is selected, remove everything + } else if( selectedText == value ){ + typedText = ""; + thismenu.setValue(""); + return false; + } + // if the user pressed [ENTER], [TAB], [RIGHT ARROW] or the delimiter--go to next level + } else if( e.keyCode == 9 || e.keyCode == 13 || e.keyCode == 39 || key == settings.delim ){ + // get the first child item if there is one + var $first = $current.find("> ul > li:first"); + + // update with the next child branch + if( $first.length > 0 ){ + updateValue($first); + // leave the field + } else { + // if IE6, we must deselect the selection + if( $.browser.msie ) setSelection($self[0], 0, 0); + if( e.keyCode == 9 ){ + // blur out of the field + $self.triggerHandler("blur"); + // hide the matches + hideMatches(); + // allow the tab + return true; + } else { + // blur out of the field + $self.trigger("blur"); + // hide the matches + hideMatches(); + } + } + + return false; + // if all the text is highlighted then we need to delete everything + } else if( selectedText == value ){ + typedText = ""; + compare = key; + } + + // update the match cache with all the matches + matchesCache = findMatches($lis, compare); + + // if we have some matches, populate autofill and show matches + if( matchesCache.length > 0 ){ + // update the a reference to what the user's typed + typedText = treePath.join(settings.delim) + (treePath.length > 0 ? settings.delim : "") + compare; + updateValue(matchesCache.eq(0), true); + } else { + // find the previous compare string + compare = compare.length ? compare.substring(0, compare.length-1) : ""; + + // since we have no matches, get the previous matches + matchesCache = findMatches($lis, compare); + + // if we have some matches, show them + if( matchesCache.length > 0 ) + showMatches(matchesCache); + // hide the matches + else + hideMatches(); + } + + // stop default behavior + e.preventDefault(); + + return false; + }; + + function moveMatch(step){ + // find the current item in the matches cache + var $current = getCurrentListItem(), $next, pos = 0; + + // if nothing selected, look for the item with the hover class + if( $current.length == 0 ) $current = matchesCache.filter(".mc_hover, .mc_hover_parent"); + // if still nothing, grab the first item in the cache + if( $current.length == 0 || $keylist.is(":hidden") ){ + // grab the first item + $current = matchesCache.eq(0); + // since nothing is selected, don't step forward/back + step = 0; + } + + // find the current position of the element + matchesCache.each(function (i){ + if( this === $current[0]){ + pos = i; + return false; + } + }); + + // if no matches, cancel + if( !matchesCache || matchesCache.length == 0 || $current.length == 0 ) return false; + + // adjust by the step count + pos = pos + step; + + // make sure pos is in valid bounds + if( pos < 0 ) pos = matchesCache.length-1; + else if( pos >= matchesCache.length ) pos = 0; + + // get the next item + $next = matchesCache.eq(pos); + + updateValue($next, true); + }; + + function findMatches($lis, compare){ + var matches = $([]); // $([]) = empty jquery object + + $lis.each(function (){ + // get the current list item and it's label + var $li = $(this), label = getNodeText($li); + + // label matches what the user typed, add it to the queue + if( label.substring(0, compare.length).toLowerCase() == compare ){ + // store a copy to this jQuery item + matches = matches.add($li); + } + }); + + // return the matches found + return matches; + }; + + function updateValue($li, keepTypedText){ + // grab all direct children items + var $siblings = keepTypedText ? matchesCache : ($li.length == 0 || $li.hasClass("mc_root")) ? $list.find("> li") : $li.parent().find("> li"); + var treePath = getTreePath($li); + var currentNode = treePath.pop().toLowerCase(); + + // update the a reference to what the user's typed + if( !keepTypedText ) typedText = treePath.join(settings.delim).toLowerCase() + (treePath.length > 0 ? settings.delim : ""); + + // update form field and display with the updated value + thismenu.setValue($li.attr(settings.valueAttr), true); + + // pre-select the last node + setSelection($self[0], typedText.length, currentNode.length+typedText.length); + + // remove any currently selected items + $siblings.filter(".mc_hover,.mc_hover_parent").removeClass("mc_hover mc_hover_parent"); + // add the hover class + $li.addClass("mc_hover" + ($li.is(".mc_parent")? "_parent" : "")); + + // show all the matches + showMatches($siblings); + }; + + // get the text currently selected by the user in a text field + function getSelection(field){ + var text = ""; + if( field.setSelectionRange ){ + text = field.value.substring(field.selectionStart, field.selectionEnd); + } else if( document.selection ){ + var range = document.selection.createRange(); + if( range.parentElement() == field ){ + text = range.text; + } + } + return text; + }; + + // set the text selected in a text field + function setSelection(field, start, end) { + if( field.createTextRange ){ + var selRange = field.createTextRange(); + selRange.collapse(true); + selRange.moveStart("character", start); + selRange.moveEnd("character", end); + selRange.select(); + } else if( field.setSelectionRange ){ + field.setSelectionRange(start, end); + } else { + if( field.selectionStart ){ + field.selectionStart = start; + field.selectionEnd = end; + } + } + field.focus(); + }; + + function scrollIntoView($el, center){ + var el = $el[0]; + var scrollable = $keylist[0]; + // get the padding which is need to adjust the scrollTop + var s = {pTop: parseInt($keylist.css("paddingTop"), 10)||0, pBottom: parseInt($keylist.css("paddingBottom"), 10)||0, bTop: parseInt($keylist.css("borderTopWidth"), 10)||0, bBottom: parseInt($keylist.css("borderBottomWidth"), 10)||0}; + + // scrolling down + if( (el.offsetTop + el.offsetHeight) > (scrollable.scrollTop + scrollable.clientHeight) ){ + scrollable.scrollTop = $el.offset().top + (scrollable.scrollTop - $keylist.offset().top) - ((scrollable.clientHeight/((center == true) ? 2 : 1)) - ($el.outerHeight() + s.pBottom)); + // scrolling up + } else if( el.offsetTop - s.bTop - s.bBottom <= (scrollable.scrollTop + s.pTop + s.pBottom) ){ + scrollable.scrollTop = $el.offset().top + (scrollable.scrollTop - $keylist.offset().top) - s.pTop; + } + }; + + // run the init callback (some keyboard entry methods will manage this callback manually) + if( settings.init != null ) settings.init.apply(thismenu, [$input, $hidden, $list]); + + }; + +})(jQuery); -- cgit v1.1