pretty-dropdowns
Advanced tools
Comparing version 3.1.3 to 3.3.3
/*! | ||
* jQuery Pretty Dropdowns Plugin v3.1.3 by T. H. Doan (http://thdoan.github.io/pretty-dropdowns/) | ||
* jQuery Pretty Dropdowns Plugin v3.3.3 by T. H. Doan (http://thdoan.github.io/pretty-dropdowns/) | ||
* | ||
@@ -17,5 +17,3 @@ * jQuery Pretty Dropdowns by T. H. Doan is licensed under the MIT License. | ||
}, oOptions); | ||
var nHoverIndex, | ||
nLastIndex, | ||
nTimer, | ||
var $current, | ||
aKeys = [ | ||
@@ -25,55 +23,83 @@ '0','1','2','3','4','5','6','7','8','9',,,,,,,, | ||
], | ||
$current, | ||
nHoverIndex, | ||
nLastIndex, | ||
nTimer, | ||
handleKeypress = function(e) { | ||
var $dropdown = $('.prettydropdown > ul.active'), | ||
var $dropdown = $('.prettydropdown > ul.active, .prettydropdown > ul:focus'); | ||
if (!$dropdown.length) return; | ||
if (e.which===9) { // Tab | ||
resetDropdown($dropdown[0]); | ||
return; | ||
} else { | ||
// Intercept non-Tab keys only | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
} | ||
var $items = $dropdown.children(), | ||
bOpen = $dropdown.hasClass('active'), | ||
nItemsHeight = $dropdown.height()/(oOptions.height-2), | ||
nItemsPerPage = nItemsHeight%1<0.5 ? Math.floor(nItemsHeight) : Math.ceil(nItemsHeight), | ||
sKey; | ||
if (!$dropdown.length) return; | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
nHoverIndex = $dropdown.children('li.hover').index(); | ||
nLastIndex = $dropdown.children().length-1; | ||
$current = $dropdown.children().eq(nHoverIndex); | ||
nHoverIndex = Math.max(0, $dropdown.children('li.hover').index()); | ||
nLastIndex = $items.length-1; | ||
$current = $items.eq(nHoverIndex); | ||
$dropdown.data('lastKeypress', +new Date()); | ||
switch (e.which) { | ||
case 13: // Enter | ||
if (!bOpen) toggleHover($current, 1); | ||
$current.click(); | ||
return; | ||
break; | ||
case 27: // Esc | ||
resetDropdown($dropdown[0]); | ||
return; | ||
if (bOpen) resetDropdown($dropdown[0]); | ||
break; | ||
case 32: // Space | ||
sKey = ' '; | ||
if (bOpen) { | ||
sKey = ' '; | ||
} else { | ||
toggleHover($current, 1); | ||
$current.click(); | ||
} | ||
break; | ||
case 33: // Page Up | ||
toggleHover($current, 0); | ||
toggleHover($dropdown.children().eq(Math.max(nHoverIndex-nItemsPerPage-1, 0)), 1); | ||
return; | ||
if (bOpen) { | ||
toggleHover($current, 0); | ||
toggleHover($items.eq(Math.max(nHoverIndex-nItemsPerPage-1, 0)), 1); | ||
} | ||
break; | ||
case 34: // Page Down | ||
toggleHover($current, 0); | ||
toggleHover($dropdown.children().eq(Math.min(nHoverIndex+nItemsPerPage-1, nLastIndex)), 1); | ||
return; | ||
if (bOpen) { | ||
toggleHover($current, 0); | ||
toggleHover($items.eq(Math.min(nHoverIndex+nItemsPerPage-1, nLastIndex)), 1); | ||
} | ||
break; | ||
case 35: // End | ||
toggleHover($current, 0); | ||
toggleHover($dropdown.children().eq(nLastIndex), 1); | ||
return; | ||
if (bOpen) { | ||
toggleHover($current, 0); | ||
toggleHover($items.eq(nLastIndex), 1); | ||
} | ||
break; | ||
case 36: // Home | ||
toggleHover($current, 0); | ||
toggleHover($dropdown.children().eq(0), 1); | ||
return; | ||
if (bOpen) { | ||
toggleHover($current, 0); | ||
toggleHover($items.eq(0), 1); | ||
} | ||
break; | ||
case 38: // Up | ||
toggleHover($current, 0); | ||
// If not already key-navigated or first item is selected, cycle to the last item; | ||
// else select the previous item | ||
toggleHover(nHoverIndex ? $dropdown.children().eq(nHoverIndex-1) : $dropdown.children().eq(nLastIndex), 1); | ||
return; | ||
if (bOpen) { | ||
toggleHover($current, 0); | ||
// If not already key-navigated or first item is selected, cycle to | ||
// the last item; or else select the previous item | ||
toggleHover(nHoverIndex ? $items.eq(nHoverIndex-1) : $items.eq(nLastIndex), 1); | ||
} | ||
break; | ||
case 40: // Down | ||
toggleHover($current, 0); | ||
// If last item is selected, cycle to the first item; | ||
// else select the next item | ||
toggleHover(nHoverIndex===nLastIndex ? $dropdown.children().eq(0) : $dropdown.children().eq(nHoverIndex+1), 1); | ||
return; | ||
if (bOpen) { | ||
toggleHover($current, 0); | ||
// If last item is selected, cycle to the first item; or else select | ||
// the next item | ||
toggleHover(nHoverIndex===nLastIndex ? $items.eq(0) : $items.eq(nHoverIndex+1), 1); | ||
} | ||
break; | ||
default: | ||
sKey = aKeys[e.which-48]; | ||
if (bOpen) sKey = aKeys[e.which-48]; | ||
} | ||
@@ -85,7 +111,8 @@ if (sKey) { // Alphanumeric key pressed | ||
$dropdown.removeData('keysPressed'); | ||
// NOTE: Windows keyboard repeat delay is 250-1000 ms. See | ||
// https://technet.microsoft.com/en-us/library/cc978658.aspx | ||
}, 300); | ||
// Build index of matches | ||
var aMatches = [], | ||
nCurrentIndex = $current.index(), | ||
$items = $dropdown.children(); | ||
nCurrentIndex = $current.index(); | ||
$items.each(function(nIndex) { | ||
@@ -110,33 +137,44 @@ if ($(this).text().toLowerCase().indexOf($dropdown.data('keysPressed'))===0) aMatches.push(nIndex); | ||
}, | ||
hoverDropdownItem = function(e) { | ||
var $dropdown = $(e.currentTarget); | ||
if ($dropdown[0]===e.target || !$dropdown.hasClass('active') || new Date()-$dropdown.data('lastKeypress')<200) return; | ||
toggleHover($dropdown.children(), 0, 1); | ||
toggleHover($(e.target), 1, 1); | ||
}, | ||
resetDropdown = function(o) { | ||
var $dropdown = $(o.currentTarget||o); | ||
// NOTE: Sometimes it's possible for $dropdown to point to the wrong | ||
// element when you quickly hover over another menu. To prevent this, we | ||
// need to check for .active as a backup. | ||
if (!$dropdown.hasClass('active')) $dropdown = $('.prettydropdown > ul.active'); | ||
$dropdown.data('hover', false); | ||
clearTimeout(nTimer); | ||
nTimer = setTimeout(function() { | ||
if (!$dropdown.data('hover')) { | ||
if ($dropdown.hasClass('reverse')) $dropdown.prepend($dropdown.children('li:last-child')); | ||
$dropdown.removeClass('active changing reverse').css('height', ''); | ||
$dropdown.children().removeClass('hover nohover'); | ||
$dropdown.removeData('clicked'); | ||
$(window).off('keydown', handleKeypress); | ||
} | ||
if ($dropdown.data('hover')) return; | ||
if ($dropdown.hasClass('reverse')) $dropdown.prepend($dropdown.children('li:last-child')); | ||
$dropdown.removeClass('active reverse').css('height', ''); | ||
$dropdown.children().removeClass('hover nohover'); | ||
$dropdown.removeData('clicked').attr('aria-expanded', 'false'); | ||
}, (o.type==='mouseleave' && !$dropdown.data('clicked')) ? oOptions.hoverIntent : 0); | ||
}, | ||
hoverDropdownItem = function(e) { | ||
var $dropdown = $(e.currentTarget); | ||
if (!$dropdown.hasClass('active') || new Date()-$dropdown.data('lastKeypress')<200) return; | ||
toggleHover($dropdown.children(), 0, 1); | ||
toggleHover($(e.target), 1, 1); | ||
selectDropdownItem = function($li) { | ||
var $dropdown = $li.parent(), | ||
$select = $dropdown.parent().find('select'); | ||
$dropdown.children('li.selected').removeClass('selected'); | ||
$dropdown.prepend($li.addClass('selected')).removeClass('reverse').attr('aria-activedescendant', $li.attr('id')); | ||
// Sync <select> element | ||
$select.children('option[value="' + $li.data('value') +'"]').prop('selected', true); | ||
$select.trigger('change'); | ||
}, | ||
toggleHover = function($el, bOn, bNoScroll) { | ||
toggleHover = function($li, bOn, bNoScroll) { | ||
if (bOn) { | ||
$el.removeClass('nohover').addClass('hover'); | ||
if ($el.length===1 && $current && !bNoScroll) { | ||
$li.removeClass('nohover').addClass('hover'); | ||
if ($li.length===1 && $current && !bNoScroll) { | ||
// Ensure items are always in view | ||
var $dropdown = $el.parent(), | ||
var $dropdown = $li.parent(), | ||
nDropdownHeight = $dropdown.outerHeight(), | ||
nItemOffset = $el.offset().top-$dropdown.offset().top-1; // -1px for top border | ||
if ($el.index()===0) { | ||
nItemOffset = $li.offset().top-$dropdown.offset().top-1; // -1px for top border | ||
if ($li.index()===0) { | ||
$dropdown.scrollTop(0); | ||
} else if ($el.index()===nLastIndex) { | ||
} else if ($li.index()===nLastIndex) { | ||
$dropdown.scrollTop($dropdown.children().length*oOptions.height); | ||
@@ -149,3 +187,3 @@ } else { | ||
} else { | ||
$el.removeClass('hover').addClass('nohover'); | ||
$li.removeClass('hover').addClass('nohover'); | ||
} | ||
@@ -157,30 +195,39 @@ }; | ||
return this.each(function() { | ||
var $this = $(this); | ||
if ($this.data('loaded')) return true; // Continue | ||
$this.outerHeight(oOptions.height); | ||
// NOTE: $this.css('margin') returns empty string in Firefox. | ||
// See https://github.com/jquery/jquery/issues/3383 | ||
var nWidth = $this.outerWidth(), | ||
var $select = $(this); | ||
if ($select.data('loaded')) return true; // Continue | ||
$select.outerHeight(oOptions.height); | ||
var nCount = 0, | ||
nTimestamp = +new Date(), | ||
nWidth = $select.outerWidth(), | ||
// Height - 2px for borders | ||
sHtml = '<ul' + ($this.attr('title')?' title="'+$this.attr('title')+'"':'') + ' style="max-height:' + (oOptions.height-2) + 'px;margin:' | ||
+ $this.css('margin-top') + ' ' | ||
+ $this.css('margin-right') + ' ' | ||
+ $this.css('margin-bottom') + ' ' | ||
+ $this.css('margin-left') + ';">', | ||
sHtml = '<ul' + ($select.attr('title')?' title="'+$select.attr('title')+'"':'') | ||
+ ' tabindex="0" role="listbox" aria-activedescendant="item' + nTimestamp | ||
+ '-1" aria-expanded="false" style="max-height:' + (oOptions.height-2) + 'px;margin:' | ||
// NOTE: $select.css('margin') returns empty string in Firefox. See | ||
// https://github.com/jquery/jquery/issues/3383 | ||
+ $select.css('margin-top') + ' ' | ||
+ $select.css('margin-right') + ' ' | ||
+ $select.css('margin-bottom') + ' ' | ||
+ $select.css('margin-left') + ';">', | ||
renderItem = function(el, sClass) { | ||
return '<li data-value="' + el.value + '"' | ||
return '<li id="item' + nTimestamp + '-' + nCount | ||
+ '" data-value="' + el.value + '"' | ||
+ (el.title ? ' title="' + el.title + '"' : '') | ||
+ ' role="option"' | ||
+ (sClass ? ' class="' + sClass + '"' : '') | ||
+ ((oOptions.height!==50) ? ' style="height:' + (oOptions.height-2) + 'px;line-height:' + (oOptions.height-2) + 'px"' : '') | ||
+ ((oOptions.height!==50) ? ' style="height:' + (oOptions.height-2) | ||
+ 'px;line-height:' + (oOptions.height-2) + 'px"' : '') | ||
+ '>' + el.text + '</li>'; | ||
}; | ||
$this.children('option:selected').each(function() { | ||
$select.children('option:selected').each(function() { | ||
++nCount; | ||
sHtml += renderItem(this, 'selected'); | ||
}); | ||
$this.children('option:not(:selected)').each(function() { | ||
$select.children('option:not(:selected)').each(function() { | ||
++nCount; | ||
sHtml += renderItem(this); | ||
}); | ||
sHtml += '</ul>'; | ||
$this.css('visibility', 'hidden').wrap('<div class="prettydropdown ' + oOptions.customClass + ' loading"></div>').before(sHtml).data('loaded', true); | ||
var $dropdown = $this.parent().children('ul'), | ||
$select.css('visibility', 'hidden').wrap('<div class="prettydropdown ' + oOptions.customClass + ' loading"></div>').before(sHtml).data('loaded', true); | ||
var $dropdown = $select.parent().children('ul'), | ||
nWidth = $dropdown.outerWidth(true), | ||
@@ -204,12 +251,6 @@ nOuterWidth; | ||
// is a scrollbar. | ||
$dropdown.children('li').width(nWidth); | ||
$dropdown.children('li').css('width', $dropdown.children('li').css('width')).click(function() { | ||
var $li = $(this); | ||
$dropdown.children().width(nWidth).css('width', $dropdown.children().css('width')).click(function() { | ||
// Only update if different value selected | ||
if ($dropdown.hasClass('active') && $li.data('value')!==$dropdown.children('li.selected').data('value')) { | ||
$dropdown.children('li.selected').removeClass('selected'); | ||
$dropdown.prepend($li.addClass('selected')).removeClass('reverse'); | ||
// Sync <select> element | ||
$this.children('option[value="' + $li.data('value') +'"]').prop('selected', true); | ||
$this.trigger('change'); | ||
if ($dropdown.hasClass('active') && $(this).data('value')!==$dropdown.children('li.selected').data('value')) { | ||
selectDropdownItem($(this)); | ||
} | ||
@@ -219,2 +260,8 @@ $dropdown.toggleClass('active'); | ||
if ($dropdown.hasClass('active')) { | ||
// Ensure the selected item is in view | ||
$dropdown.scrollTop(0); | ||
// Close any other open menus | ||
if ($('.prettydropdown > ul.active').length>1) { | ||
resetDropdown($('.prettydropdown > ul.active').not($dropdown)[0]); | ||
} | ||
var nWinHeight = window.innerHeight, | ||
@@ -237,5 +284,5 @@ nOffsetTop = $dropdown.offset().top, | ||
} | ||
$(window).on('keydown', handleKeypress); | ||
$dropdown.attr('aria-expanded', 'true'); | ||
} else { | ||
$dropdown.addClass('changing').data('clicked', true); // Prevent FOUC | ||
$dropdown.data('clicked', true); | ||
resetDropdown($dropdown[0]); | ||
@@ -245,2 +292,9 @@ } | ||
$dropdown.on({ | ||
focusin: function() { | ||
// Unregister any existing handlers first to prevent duplicate firings | ||
$(window).off('keydown', handleKeypress).on('keydown', handleKeypress); | ||
}, | ||
focusout: function() { | ||
$(window).off('keydown', handleKeypress); | ||
}, | ||
mouseenter: function() { | ||
@@ -247,0 +301,0 @@ $dropdown.data('hover', true); |
{ | ||
"name": "pretty-dropdowns", | ||
"version": "3.1.3", | ||
"version": "3.3.3", | ||
"description": "A simple, lightweight jQuery plugin to create stylized drop-down menus.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
# jQuery Pretty Dropdowns | ||
Pretty Dropdowns is a simple, lightweight jQuery plugin that converts `<select>` drop-down menus into "pretty" menus that you can style using CSS. Full keyboard navigation is supported (you can go directly to a menu option by typing its text). As an extra bonus, it does its best to keep the menu options within the viewport. | ||
Pretty Dropdowns is a simple, lightweight jQuery plugin that converts `<select>` drop-down menus into "pretty" menus that you can style using CSS. | ||
### Features: | ||
- Two arrow styles and sizes to choose from (or add your own style) | ||
- Full keyboard navigation support (you can even go directly to a menu item by typing its text) | ||
- Accessible (it plays nicely with screen readers) | ||
- Sensible (when you open the menu it does its best to keep the menu items within the viewport) | ||
**[See a demo »](https://thdoan.github.io/pretty-dropdowns/demo.html)** | ||
@@ -51,2 +58,18 @@ | ||
## Keyboard Navigation | ||
Key | Description | ||
------- | ----------- | ||
`Tab` | Put focus on the next drop-down menu. If a menu is open, it will automatically close. | ||
`Shift`+`Tab` | Put focus on the previous drop-down menu. If a menu is open, it will automatically close. | ||
`Enter` | Open the drop-down menu that is in focus. If it is already open, then select the highlighted item. | ||
`Esc` | Close the drop-down menu. | ||
`Home` | Jump to the first item in the drop-down menu. | ||
`End` | Jump to the last item in the drop-down menu. | ||
`PgUp` | Go to the previous page of items. If there is no scrollbar, then this is the same as `Home`. | ||
`PgDn` | Go to the next page of items. If there is no scrollbar, then this is the same as `End`. | ||
`Up` | Highlight the previous item in the drop-down menu. | ||
`Down` | Highlight the next item in the drop-down menu. | ||
`A`-`Z`<br>`0`-`9`<br>`Space` | If the drop-down menu is open, jump to the first item matching the key(s) pressed. Every time you press a key it will cycle through the matching items. **Hint:** if you type fast enough, it will try to find a match for everything you typed instead of just the first character. If the menu is closed and in focus, `Space` opens the menu (same as `Enter`). | ||
## Installation | ||
@@ -53,0 +76,0 @@ |
Sorry, the diff of this file is not supported yet
21743
424
83