├── listmenu.css ├── README.md └── jquery.listmenu.js /listmenu.css: -------------------------------------------------------------------------------- 1 | /* 2 | STYLE SHEET FOR IHWY JQUERY LISTMENU PLUGIN V 1.0, 3/2/2009 3 | 4 | For more information, visit http://www.ihwy.com/Labs/jquery-listmenu-plugin.aspx 5 | */ 6 | 7 | 8 | /* default styling example 9 | ----------------------------------------------------------------- */ 10 | 11 | .lm-wrapper { margin:0; padding:0; } 12 | .lm-wrapper .lm-letters { overflow:hidden; } 13 | * html .lm-wrapper .lm-letters { zoom:1; } /* for IE6 so that menu appears under letters */ 14 | .lm-wrapper .lm-letters a { font-size:0.9em; display:block; float:left; padding:2px 11px; border:1px solid silver; border-right:none; text-decoration:none; } 15 | .lm-wrapper .lm-letters a:hover, 16 | .lm-wrapper .lm-letters a.lm-selected { background-color:#eaeaea; } 17 | .lm-wrapper .lm-letters a.lm-disabled { color:#ccc; } 18 | .lm-wrapper .lm-letters a.lm-last { border-right:1px solid silver; } 19 | .lm-wrapper .lm-letter-count { text-align:center; font-size:0.8em; line-height:1; margin-bottom:3px; color:#336699; } 20 | 21 | .lm-wrapper .lm-menu { border:1px solid silver; border-top:1px solid silver; padding:15px; z-index:10; position:absolute; margin-top:-1px; background:#ffc; display:none; } 22 | .lm-wrapper .lm-menu ul li { list-style-type:none; margin-bottom:5px; font-size:0.9em } 23 | .lm-wrapper .lm-menu ol li { margin-left:15px; } 24 | .lm-wrapper .lm-menu .lm-no-match { color:green; } 25 | .lm-wrapper .lm-menu a { text-decoration:none; } 26 | .lm-wrapper .lm-menu a:hover { text-decoration:underline; } 27 | .lm-wrapper .lm-menu .lm-submenu { overflow:hidden; } 28 | 29 | /* extra styling for some of the specific demos 30 | ------------------------------------------------------------------ */ 31 | 32 | #demo5-menu .lm-menu div div div div { border:1px solid silver; padding:5px; margin-bottom:1em; } 33 | #demo5-menu .lm-menu div div div a { display:block; margin-bottom:1em; } 34 | #demo5-menu .lm-menu div div div p.subtitle { font-weight:bold; color:blue; } 35 | 36 | #demo6-menu .lm-menu ul li { border:1px solid silver; padding:5px; } 37 | #demo6-menu .lm-menu ul li a { font-weight:bold; } 38 | #demo6-menu .lm-menu ul li p { padding-bottom:0; } 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery ListMenu Plugin 2 | 3 | This jQuery plugin, developed in the iHwy Labs, allows you to easily convert a 4 | long, hard to navigate list into a compact, easily skimmable 'first-letter' 5 | based menuing system, allowing quick and 'out-of the-way' access to hundreds 6 | of items. Users hover their mouse over a letter and a columnized list of all 7 | of the list items that start with that letter appear in a submenu. Mousing off 8 | of the letter or menu closes the submenu. Mousing between letters is very fast 9 | and the columns in the submenu are nicely balanced. 10 | 11 | This is great for product lists, address books, contact lists, lists of 12 | hotels, parks and recreation areas, etc. 13 | 14 | [View the Demos](http://cdn.ihwy.net/ihwy-com/labs/demos/jquery-listmenu.html) 15 | 16 | ## Highlights 17 | 18 | * Easy to unobtrusively add to existing lists of HTML elements. 19 | * Works nicely with UL and OL lists as well as any 'list' of HTML elements (child elements under a parent element). 20 | * Uses the first found letter of "actual text" in each list item (even if the text is nested inside multiple HTML tags) to determine what navigation letter to put the item under. 21 | * Creates balanced-height columns in the dropdown menu, taking into account the actual height of each element, rather than just going by count. 22 | * If your list is an OL, numbering in each submenu starts at 1 and is carried across columns, top to bottom, left to right, maintaining a logical sequence. 23 | * Optional hovering "record count" over each letter shows user how many items are under the letter. 24 | * Optional '[0-9]' menu item for access to list items that start with a number. 25 | * Optional '[...]' menu item for access to list items that start with punctuation or chars like Ä and Ü. 26 | * Optionally set the text that appears if a letter with no list items is clicked. 27 | * Designed with CSS styling in mind. Style all aspects of the list navigation and dropdown menu via CSS. 28 | * Make letters with no list items appear "disabled" using an optional CSS class. 29 | 30 | ## More Information 31 | 32 | For complete info about how to use this plugin, see the [jQuery ListMenu](http://www.ihwy.com/labs/jquery-listmenu-plugin.aspx) page at the [iHwy, Inc.](http://www.ihwy.com) site. 33 | 34 | -------------------------------------------------------------------------------- /jquery.listmenu.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * jQuery listmenu plugin 4 | * Copyright (c) 2009 iHwy, Inc. 5 | * Author: Jack Killpatrick 6 | * 7 | * Version 1.1 (08/09/2009) 8 | * Requires jQuery 1.3.2 or jquery 1.2.6 9 | * 10 | * Visit http://www.ihwy.com/labs/jquery-listmenu-plugin.aspx for more information. 11 | * 12 | * Dual licensed under the MIT and GPL licenses: 13 | * http://www.opensource.org/licenses/mit-license.php 14 | * http://www.gnu.org/licenses/gpl.html 15 | * 16 | */ 17 | 18 | (function($) { 19 | $.fn.listmenu = function(options) { 20 | var opts = $.extend({}, $.fn.listmenu.defaults, options); 21 | var alph = ['_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '-']; 22 | 23 | return this.each(function() { 24 | var $wrapper, list, $list, $letters, letters = {}, $letterCount, id, $menu, colOpts = $.extend({}, $.fn.listmenu.defaults.cols, opts.cols), onNav = false, onMenu = false, currentLetter = ''; 25 | id = this.id; 26 | $list = $(this); 27 | 28 | function init() { 29 | $list.css('visibility', 'hidden'); // hiding to prevent pre-load flicker. Using visibility:hidden so that list item dimensions remain available 30 | 31 | setTimeout(function() { 32 | $list.before(createWrapperHtml()); 33 | 34 | $wrapper = $('#' + id + '-menu'); 35 | $wrapper.append(createLettersHtml()); 36 | 37 | $letters = $('.lm-letters', $wrapper).slice(0, 1); // will always be a single item 38 | if (opts.showCounts) $letterCount = $('.lm-letter-count', $wrapper).slice(0, 1); // will always be a single item 39 | 40 | $wrapper.append(createMenuHtml()); 41 | $menu = $('.lm-menu', $wrapper); 42 | populateMenu(); 43 | if (opts.flagDisabled) addDisabledClass(); // run after populateMenu(): needs some data from there 44 | 45 | bindHandlers(); 46 | 47 | // decide whether to include num and/or other links 48 | // 49 | if (!opts.includeNums) $('._', $letters).remove(); 50 | if (!opts.includeOther) $('.-', $letters).remove(); 51 | $(':last', $letters).addClass('lm-last'); 52 | 53 | $wrapper.show(); 54 | }, 50); 55 | } 56 | 57 | // positions the letter count div above the letter links (so we only have to do it once: after this we just change it's left position via mouseover) 58 | // 59 | function setLetterCountTop() { 60 | $letterCount.css({ top: $('.a', $letters).slice(0, 1).offset({ margin: false, border: true }).top - $letterCount.outerHeight({ margin: true }) }); 61 | } 62 | 63 | function addDisabledClass() { 64 | for (var i = 0; i < alph.length; i++) { 65 | if (letters[alph[i]] == undefined) $('.' + alph[i], $letters).addClass('lm-disabled'); 66 | } 67 | } 68 | 69 | function populateMenu() { 70 | var gutter = colOpts.gutter, 71 | cols = colOpts.count, 72 | menuWidth, 73 | colWidth; 74 | 75 | if (opts.menuWidth) menuWidth = opts.menuWidth; // use user defined menu width if one provided, else calculate one 76 | else menuWidth = $('.lm-letters', $wrapper).width() - ($menu.outerWidth() - $menu.width()); 77 | 78 | colWidth = (cols == 1) ? menuWidth : Math.floor((menuWidth - (gutter * (cols - 1))) / cols); 79 | $menu.width(menuWidth); // prevents it from resizing based on content 80 | 81 | $list.width(colWidth); 82 | 83 | var letter, outerHeight; 84 | $list.children().each(function() { 85 | str = $(this).text().replace(/\s+/g, ''); // strip all white space from text (including tabs and linebreaks that might have been in the HTML) // thanks to Liam Byrne, liam@onsight.ie 86 | if (str != '') { 87 | firstChar = str.slice(0, 1).toLowerCase(); 88 | if (/\W/.test(firstChar)) firstChar = '-'; // not A-Z, a-z or 0-9, so considered "other" 89 | if (!isNaN(firstChar)) firstChar = '_'; // use '_' if the first char is a number 90 | } 91 | 92 | outerHeight = $(this).outerHeight(); 93 | 94 | if (letters[firstChar] == undefined) letters[firstChar] = { totHeight: 0, count: 0, colHeight: 0, hasMenu: false }; 95 | letter = letters[firstChar]; 96 | letter.totHeight += outerHeight; 97 | letter.count++; 98 | 99 | $.data($(this)[0], id, { firstChar: firstChar, height: outerHeight }); 100 | }); 101 | 102 | $.each(letters, function() { 103 | this.colHeight = (this.count > 1) ? Math.ceil(this.totHeight / cols) : this.totHeight; 104 | }); 105 | 106 | var $this, data, iHeight, iLetter, cols = {}, c; 107 | var tagName = $list[0].tagName.toLowerCase(); 108 | $list.children().each(function() { 109 | $this = $(this); 110 | data = $.data($this.get(0), id); iLetter = data.firstChar; iHeight = data.height; 111 | if (!letters[l = iLetter].hasMenu) { 112 | $menu.append(''); 113 | letters[iLetter].hasMenu = true; 114 | } 115 | 116 | if (cols[iLetter] == undefined) cols[iLetter] = { height: 0, colNum: 0, itemCount: 0, $colRoot: null }; 117 | c = cols[iLetter]; 118 | c.itemCount++; 119 | if (c.height == 0) { 120 | c.colNum++; 121 | $('.lm-submenu.' + iLetter, $menu).append('
<' + tagName + ((tagName == 'ol') ? ' start="' + c.itemCount + '"' : '') + ' id="lm-' + id + '-' + iLetter + '-' + c.colNum + '" class="lm-col-root">
'); // reset start number for OL lists (deprecacted, but no reliable css solution). Creating an id to make lookups for appending list items faster 122 | } 123 | $('#lm-' + id + '-' + iLetter + '-' + c.colNum).append($(this)); 124 | 125 | c.height += iHeight; 126 | if (c.height >= letters[iLetter].colHeight) c.height = 0; // forces another column to get started if this letter comes up again 127 | }); 128 | 129 | $.each(letters, function(idx) { 130 | if (this.hasMenu) { 131 | $('.lm-submenu.' + idx + ' .lm-col', $menu).css({ 'width': colWidth, 'float': 'left' }); 132 | $('.lm-submenu.' + idx + ' .lm-col:not(:last)', $menu).css({ 'marginRight': gutter }); 133 | } 134 | }); 135 | 136 | $menu.append(''); 137 | $list.remove(); 138 | } 139 | 140 | function getLetterCount(el) { 141 | var letter = letters[$(el).attr('class').split(' ')[0]]; 142 | return (letter != undefined) ? letter.count : 0; // some letters may not be in the hash 143 | } 144 | 145 | function hideCurrentSubmenu() { 146 | if (currentLetter != '') $('.lm-submenu.' + currentLetter, $menu).hide(); 147 | $('.lm-no-match', $menu).hide(); // hiding each time, rather than checking to see if we need to hide it 148 | } 149 | 150 | function bindHandlers() { 151 | 152 | // sets the top position of the count div in case something above it on the page has resized 153 | // 154 | if (opts.showCounts) { 155 | $wrapper.mouseover(function() { 156 | setLetterCountTop(); 157 | }); 158 | } 159 | 160 | // kill letter clicks 161 | // 162 | $('a', $letters).click(function() { 163 | $(this).blur(); 164 | return false; 165 | }); 166 | 167 | $letters.hover( 168 | function() { onNav = true; }, 169 | function() { 170 | onNav = false; 171 | 172 | setTimeout(function() { 173 | if (!onMenu) { 174 | $('a.lm-selected', $letters).removeClass('lm-selected'); 175 | $('.lm-menu', $wrapper).hide(); 176 | hideCurrentSubmenu(); 177 | currentLetter = ''; 178 | } 179 | }, 10); 180 | } 181 | ); 182 | 183 | $('a', $letters).mouseover(function() { 184 | var count = getLetterCount(this); 185 | var $this = $(this); 186 | 187 | if (opts.showCounts) { 188 | var left = $this.position().left; 189 | var width = $this.outerWidth({ margin: true }); 190 | if (opts.showCounts) $letterCount.css({ left: left, width: width + 'px' }).text(count).show(); // set left position and width of letter count, set count text and show it 191 | } 192 | 193 | var newLetter = $this.attr('class').split(' ')[0]; 194 | if (newLetter != currentLetter) { 195 | if (currentLetter != '') { 196 | hideCurrentSubmenu(); 197 | $('a.lm-selected', $letters).removeClass('lm-selected'); 198 | } 199 | $this.addClass('lm-selected'); 200 | 201 | if (count > 0) $('.lm-submenu.' + newLetter, $wrapper).show(); 202 | else $('.lm-no-match', $wrapper).show(); 203 | 204 | if (currentLetter == '') $('.lm-menu', $wrapper).show(); 205 | currentLetter = newLetter; 206 | } 207 | }); 208 | 209 | $('a', $letters).mouseout(function() { 210 | if (opts.showCounts) $letterCount.hide(); 211 | }); 212 | 213 | $menu.hover( 214 | function() { 215 | onMenu = true; 216 | }, 217 | function() { 218 | onMenu = false; 219 | setTimeout(function() { 220 | if (!onNav) { 221 | $('a.lm-selected', $letters).removeClass('lm-selected'); 222 | $('.lm-menu', $wrapper).hide(); 223 | hideCurrentSubmenu(); 224 | currentLetter = ''; 225 | } 226 | }, 10); 227 | } 228 | ); 229 | 230 | if (opts.onClick != null) { 231 | $menu.click(function(e) { 232 | var $target = $(e.target); 233 | opts.onClick($target); 234 | return false; 235 | }); 236 | } 237 | } 238 | 239 | // creates the HTML for the letter links 240 | // 241 | function createLettersHtml() { 242 | var html = []; 243 | for (var i = 1; i < alph.length; i++) { 244 | if (html.length == 0) html.push('0-9'); 245 | html.push('' + ((alph[i] == '-') ? '...' : alph[i].toUpperCase()) + ''); 246 | } 247 | return '
' + html.join('') + '
' + ((opts.showCounts) ? '' : ''); // the styling for letterCount is to give us a starting point for the element, which will be repositioned when made visible (ie, should not need to be styled by the user) 248 | } 249 | 250 | function createMenuHtml() { 251 | return '
'; 252 | } 253 | 254 | function createWrapperHtml() { 255 | return '
'; 256 | } 257 | 258 | init(); 259 | }); 260 | }; 261 | 262 | $.fn.listmenu.defaults = { 263 | includeNums: true, 264 | includeOther: false, 265 | flagDisabled: true, 266 | noMatchText: 'No matching entries', 267 | showCounts: true, 268 | menuWidth: null, 269 | cols: { 270 | count: 4, 271 | gutter: 40 272 | }, 273 | onClick: null 274 | }; 275 | })(jQuery); 276 | --------------------------------------------------------------------------------