From aa07a7e7604394098bb65abcb1afacb74615e01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernat=20R=C3=A0fales?= Date: Mon, 28 Mar 2011 06:00:09 -0700 Subject: [PATCH 1/2] Autocomplete: added a max option to set the maximum number of suggestions to render. New #7208 - autocomplete set maximum results --- ui/jquery.ui.autocomplete.js | 263 +++++++++++++++++++++++++++++------ 1 file changed, 217 insertions(+), 46 deletions(-) diff --git a/ui/jquery.ui.autocomplete.js b/ui/jquery.ui.autocomplete.js index ca06fff6584..eb45bdd00ea 100644 --- a/ui/jquery.ui.autocomplete.js +++ b/ui/jquery.ui.autocomplete.js @@ -1,5 +1,5 @@ /* - * jQuery UI Autocomplete @VERSION + * jQuery UI Autocomplete 1.8.10 * * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. @@ -11,7 +11,6 @@ * jquery.ui.core.js * jquery.ui.widget.js * jquery.ui.position.js - * jquery.ui.menu.js */ (function( $, undefined ) { @@ -19,10 +18,8 @@ var requestIndex = 0; $.widget( "ui.autocomplete", { - defaultElement: "", options: { appendTo: "body", - autoFocus: true, delay: 300, minLength: 1, position: { @@ -30,7 +27,8 @@ $.widget( "ui.autocomplete", { at: "left bottom", collision: "none" }, - source: null + source: null, + max: 0 }, pending: 0, @@ -40,8 +38,6 @@ $.widget( "ui.autocomplete", { doc = this.element[ 0 ].ownerDocument, suppressKeyPress; - this.valueMethod = this.element[ this.element.is( "input" ) ? "val" : "text" ]; - this.element .addClass( "ui-autocomplete-input" ) .attr( "autocomplete", "off" ) @@ -92,7 +88,7 @@ $.widget( "ui.autocomplete", { self.menu.select( event ); break; case keyCode.ESCAPE: - self._value( self.term ); + self.element.val( self.term ); self.close( event ); break; default: @@ -100,7 +96,7 @@ $.widget( "ui.autocomplete", { clearTimeout( self.searching ); self.searching = setTimeout(function() { // only search if the value has changed - if ( self.term != self._value() ) { + if ( self.term != self.element.val() ) { self.selectedItem = null; self.search( null, event ); } @@ -120,7 +116,7 @@ $.widget( "ui.autocomplete", { } self.selectedItem = null; - self.previous = self._value(); + self.previous = self.element.val(); }) .bind( "blur.autocomplete", function( event ) { if ( self.options.disabled ) { @@ -153,7 +149,7 @@ $.widget( "ui.autocomplete", { $( document ).one( 'mousedown', function( event ) { if ( event.target !== self.element[ 0 ] && event.target !== menuElement && - !$.contains( menuElement, event.target ) ) { + !$.ui.contains( menuElement, event.target ) ) { self.close(); } }); @@ -166,18 +162,16 @@ $.widget( "ui.autocomplete", { }, 13); }) .menu({ - // custom key handling for now - input: $(), focus: function( event, ui ) { var item = ui.item.data( "item.autocomplete" ); if ( false !== self._trigger( "focus", event, { item: item } ) ) { // use value to match what will end up in the input, if it was a key event if ( /^key/.test(event.originalEvent.type) ) { - self._value( item.value ); + self.element.val( item.value ); } } }, - select: function( event, ui ) { + selected: function( event, ui ) { var item = ui.item.data( "item.autocomplete" ), previous = self.previous; @@ -195,11 +189,11 @@ $.widget( "ui.autocomplete", { } if ( false !== self._trigger( "select", event, { item: item } ) ) { - self._value( item.value ); + self.element.val( item.value ); } // reset the term after the select event // this allows custom select handling to work properly - self.term = self._value(); + self.term = self.element.val(); self.close( event ); self.selectedItem = item; @@ -208,12 +202,14 @@ $.widget( "ui.autocomplete", { // don't set the value of the text field if it's already correct // this prevents moving the cursor unnecessarily if ( self.menu.element.is(":visible") && - ( self._value() !== self.term ) ) { - self._value( self.term ); + ( self.element.val() !== self.term ) ) { + self.element.val( self.term ); } } }) .zIndex( this.element.zIndex() + 1 ) + // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781 + .css({ top: 0, left: 0 }) .hide() .data( "menu" ); if ( $.fn.bgiframe ) { @@ -221,7 +217,7 @@ $.widget( "ui.autocomplete", { } }, - _destroy: function() { + destroy: function() { this.element .removeClass( "ui-autocomplete-input" ) .removeAttr( "autocomplete" ) @@ -229,10 +225,11 @@ $.widget( "ui.autocomplete", { .removeAttr( "aria-autocomplete" ) .removeAttr( "aria-haspopup" ); this.menu.element.remove(); + $.Widget.prototype.destroy.call( this ); }, _setOption: function( key, value ) { - this._super( "_setOption", key, value ); + $.Widget.prototype._setOption.apply( this, arguments ); if ( key === "source" ) { this._initSource(); } @@ -277,15 +274,15 @@ $.widget( "ui.autocomplete", { }); }; } else { - this.source = this.options.source; + ehis.source = this.options.source; } }, search: function( value, event ) { - value = value != null ? value : this._value(); + value = value != null ? value : this.element.val(); // always save the actual value, not the one passed as an argument - this.term = this._value(); + this.term = this.element.val(); if ( value.length < this.options.minLength ) { return this.close( event ); @@ -307,11 +304,8 @@ $.widget( "ui.autocomplete", { }, _response: function( content ) { - if ( content ) { - content = this._normalize( content ); - } - this._trigger( "response", null, { content: content } ); if ( !this.options.disabled && content && content.length ) { + content = this._normalize( content ); this._suggest( content ); this._trigger( "open" ); } else { @@ -327,13 +321,13 @@ $.widget( "ui.autocomplete", { clearTimeout( this.closing ); if ( this.menu.element.is(":visible") ) { this.menu.element.hide(); - this.menu.blur(); + this.menu.deactivate(); this._trigger( "close", event ); } }, _change: function( event ) { - if ( this.previous !== this._value() ) { + if ( this.previous !== this.element.val() ) { this._trigger( "change", event, { item: this.selectedItem } ); } }, @@ -361,9 +355,9 @@ $.widget( "ui.autocomplete", { var ul = this.menu.element .empty() .zIndex( this.element.zIndex() + 1 ); - this._renderMenu( ul, items ); - // TODO refresh should check if the active item is still in the dom, removing the need for a manual blur - this.menu.blur(); + this._renderMenu( ul, items, this.options.max ); + // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate + this.menu.deactivate(); this.menu.refresh(); // size and position menu @@ -372,10 +366,6 @@ $.widget( "ui.autocomplete", { ul.position( $.extend({ of: this.element }, this.options.position )); - - if ( this.options.autoFocus ) { - this.menu.next( new $.Event("mouseover") ); - } }, _resizeMenu: function() { @@ -386,10 +376,12 @@ $.widget( "ui.autocomplete", { ) ); }, - _renderMenu: function( ul, items ) { + _renderMenu: function( ul, items, max ) { var self = this; $.each( items, function( index, item ) { - self._renderItem( ul, item ); + if ( max == 0 || index < max ) { + self._renderItem( ul, item ); + } }); }, @@ -407,8 +399,8 @@ $.widget( "ui.autocomplete", { } if ( this.menu.first() && /^previous/.test(direction) || this.menu.last() && /^next/.test(direction) ) { - this._value( this.term ); - this.menu.blur(); + this.element.val( this.term ); + this.menu.deactivate(); return; } this.menu[ direction ]( event ); @@ -416,15 +408,10 @@ $.widget( "ui.autocomplete", { widget: function() { return this.menu.element; - }, - - _value: function( value ) { - return this.valueMethod.apply( this.element, arguments ); } }); $.extend( $.ui.autocomplete, { - version: "@VERSION", escapeRegex: function( value ) { return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); }, @@ -437,3 +424,187 @@ $.extend( $.ui.autocomplete, { }); }( jQuery )); + +/* + * jQuery UI Menu (not officially released) + * + * This widget isn't yet finished and the API is subject to change. We plan to finish + * it for the next release. You're welcome to give it a try anyway and give us feedback, + * as long as you're okay with migrating your code later on. We can help with that, too. + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Menu + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function($) { + +$.widget("ui.menu", { + _create: function() { + var self = this; + this.element + .addClass("ui-menu ui-widget ui-widget-content ui-corner-all") + .attr({ + role: "listbox", + "aria-activedescendant": "ui-active-menuitem" + }) + .click(function( event ) { + if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) { + return; + } + // temporary + event.preventDefault(); + self.select( event ); + }); + this.refresh(); + }, + + refresh: function() { + var self = this; + + // don't refresh list items that are already adapted + var items = this.element.children("li:not(.ui-menu-item):has(a)") + .addClass("ui-menu-item") + .attr("role", "menuitem"); + + items.children("a") + .addClass("ui-corner-all") + .attr("tabindex", -1) + // mouseenter doesn't work with event delegation + .mouseenter(function( event ) { + self.activate( event, $(this).parent() ); + }) + .mouseleave(function() { + self.deactivate(); + }); + }, + + activate: function( event, item ) { + this.deactivate(); + if (this.hasScroll()) { + var offset = item.offset().top - this.element.offset().top, + scroll = this.element.attr("scrollTop"), + elementHeight = this.element.height(); + if (offset < 0) { + this.element.attr("scrollTop", scroll + offset); + } else if (offset >= elementHeight) { + this.element.attr("scrollTop", scroll + offset - elementHeight + item.height()); + } + } + this.active = item.eq(0) + .children("a") + .addClass("ui-state-hover") + .attr("id", "ui-active-menuitem") + .end(); + this._trigger("focus", event, { item: item }); + }, + + deactivate: function() { + if (!this.active) { return; } + + this.active.children("a") + .removeClass("ui-state-hover") + .removeAttr("id"); + this._trigger("blur"); + this.active = null; + }, + + next: function(event) { + this.move("next", ".ui-menu-item:first", event); + }, + + previous: function(event) { + this.move("prev", ".ui-menu-item:last", event); + }, + + first: function() { + return this.active && !this.active.prevAll(".ui-menu-item").length; + }, + + last: function() { + return this.active && !this.active.nextAll(".ui-menu-item").length; + }, + + move: function(direction, edge, event) { + if (!this.active) { + this.activate(event, this.element.children(edge)); + return; + } + var next = this.active[direction + "All"](".ui-menu-item").eq(0); + if (next.length) { + this.activate(event, next); + } else { + this.activate(event, this.element.children(edge)); + } + }, + + // TODO merge with previousPage + nextPage: function(event) { + if (this.hasScroll()) { + // TODO merge with no-scroll-else + if (!this.active || this.last()) { + this.activate(event, this.element.children(".ui-menu-item:first")); + return; + } + var base = this.active.offset().top, + height = this.element.height(), + result = this.element.children(".ui-menu-item").filter(function() { + var close = $(this).offset().top - base - height + $(this).height(); + // TODO improve approximation + return close < 10 && close > -10; + }); + + // TODO try to catch this earlier when scrollTop indicates the last page anyway + if (!result.length) { + result = this.element.children(".ui-menu-item:last"); + } + this.activate(event, result); + } else { + this.activate(event, this.element.children(".ui-menu-item") + .filter(!this.active || this.last() ? ":first" : ":last")); + } + }, + + // TODO merge with nextPage + previousPage: function(event) { + if (this.hasScroll()) { + // TODO merge with no-scroll-else + if (!this.active || this.first()) { + this.activate(event, this.element.children(".ui-menu-item:last")); + return; + } + + var base = this.active.offset().top, + height = this.element.height(); + result = this.element.children(".ui-menu-item").filter(function() { + var close = $(this).offset().top - base + height - $(this).height(); + // TODO improve approximation + return close < 10 && close > -10; + }); + + // TODO try to catch this earlier when scrollTop indicates the last page anyway + if (!result.length) { + result = this.element.children(".ui-menu-item:first"); + } + this.activate(event, result); + } else { + this.activate(event, this.element.children(".ui-menu-item") + .filter(!this.active || this.first() ? ":last" : ":first")); + } + }, + + hasScroll: function() { + return this.element.height() < this.element.attr("scrollHeight"); + }, + + select: function( event ) { + this._trigger("selected", event, { item: this.active }); + } +}); + +}(jQuery)); From aace207a8bff53d8cb43e8034249b9446ef8f7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernat=20R=C3=A0fales?= Date: Mon, 4 Apr 2011 05:56:14 -0700 Subject: [PATCH 2/2] Added option 'matchAnywhere' to select if the autocomplete should match the typed text everywhere or just at the beginning of the suggestion strings --- ui/jquery.ui.autocomplete.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ui/jquery.ui.autocomplete.js b/ui/jquery.ui.autocomplete.js index eb45bdd00ea..f6a72db8d39 100644 --- a/ui/jquery.ui.autocomplete.js +++ b/ui/jquery.ui.autocomplete.js @@ -28,7 +28,8 @@ $.widget( "ui.autocomplete", { collision: "none" }, source: null, - max: 0 + max: 0, + matchAnywhere: true }, pending: 0, @@ -248,7 +249,7 @@ $.widget( "ui.autocomplete", { if ( $.isArray(this.options.source) ) { array = this.options.source; this.source = function( request, response ) { - response( $.ui.autocomplete.filter(array, request.term) ); + response( $.ui.autocomplete.filter(array, request.term, self.options.matchAnywhere) ); }; } else if ( typeof this.options.source === "string" ) { url = this.options.source; @@ -415,8 +416,13 @@ $.extend( $.ui.autocomplete, { escapeRegex: function( value ) { return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); }, - filter: function(array, term) { - var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); + filter: function(array, term, matchAnywhere) { + if (matchAnywhere) { + var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); + } + else { + var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex(term), "i" ); + } return $.grep( array, function(value) { return matcher.test( value.label || value.value || value ); });