mirror of
https://github.com/jquery/jquery-ui.git
synced 2024-10-20 20:44:21 +00:00
dc67d2c5d2
For instance, this is useful for the jquery-ui-rails gem, which does not
use jQuery UI's own minification, but relies on Rails to minify the
files where necessary. Rails in turn uses UglifyJS for JS and YUI for
CSS, both of which respect the /*! ... */ convention.
(cherry picked from commit 37dcc3e21d
, dropped menu, spinner and tooltip)
625 lines
17 KiB
JavaScript
625 lines
17 KiB
JavaScript
/*!
|
|
* jQuery UI Autocomplete @VERSION
|
|
*
|
|
* Copyright 2012, 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/Autocomplete
|
|
*
|
|
* Depends:
|
|
* jquery.ui.core.js
|
|
* jquery.ui.widget.js
|
|
* jquery.ui.position.js
|
|
*/
|
|
(function( $, undefined ) {
|
|
|
|
// used to prevent race conditions with remote data sources
|
|
var requestIndex = 0;
|
|
|
|
$.widget( "ui.autocomplete", {
|
|
options: {
|
|
appendTo: "body",
|
|
autoFocus: false,
|
|
delay: 300,
|
|
minLength: 1,
|
|
position: {
|
|
my: "left top",
|
|
at: "left bottom",
|
|
collision: "none"
|
|
},
|
|
source: null
|
|
},
|
|
|
|
pending: 0,
|
|
|
|
_create: function() {
|
|
var self = this,
|
|
doc = this.element[ 0 ].ownerDocument,
|
|
suppressKeyPress;
|
|
|
|
this.element
|
|
.addClass( "ui-autocomplete-input" )
|
|
.attr( "autocomplete", "off" )
|
|
// TODO verify these actually work as intended
|
|
.attr({
|
|
role: "textbox",
|
|
"aria-autocomplete": "list",
|
|
"aria-haspopup": "true"
|
|
})
|
|
.bind( "keydown.autocomplete", function( event ) {
|
|
if ( self.options.disabled || self.element.propAttr( "readOnly" ) ) {
|
|
return;
|
|
}
|
|
|
|
suppressKeyPress = false;
|
|
var keyCode = $.ui.keyCode;
|
|
switch( event.keyCode ) {
|
|
case keyCode.PAGE_UP:
|
|
self._move( "previousPage", event );
|
|
break;
|
|
case keyCode.PAGE_DOWN:
|
|
self._move( "nextPage", event );
|
|
break;
|
|
case keyCode.UP:
|
|
self._move( "previous", event );
|
|
// prevent moving cursor to beginning of text field in some browsers
|
|
event.preventDefault();
|
|
break;
|
|
case keyCode.DOWN:
|
|
self._move( "next", event );
|
|
// prevent moving cursor to end of text field in some browsers
|
|
event.preventDefault();
|
|
break;
|
|
case keyCode.ENTER:
|
|
case keyCode.NUMPAD_ENTER:
|
|
// when menu is open and has focus
|
|
if ( self.menu.active ) {
|
|
// #6055 - Opera still allows the keypress to occur
|
|
// which causes forms to submit
|
|
suppressKeyPress = true;
|
|
event.preventDefault();
|
|
}
|
|
//passthrough - ENTER and TAB both select the current element
|
|
case keyCode.TAB:
|
|
if ( !self.menu.active ) {
|
|
return;
|
|
}
|
|
self.menu.select( event );
|
|
break;
|
|
case keyCode.ESCAPE:
|
|
self.element.val( self.term );
|
|
self.close( event );
|
|
break;
|
|
default:
|
|
// keypress is triggered before the input value is changed
|
|
clearTimeout( self.searching );
|
|
self.searching = setTimeout(function() {
|
|
// only search if the value has changed
|
|
if ( self.term != self.element.val() ) {
|
|
self.selectedItem = null;
|
|
self.search( null, event );
|
|
}
|
|
}, self.options.delay );
|
|
break;
|
|
}
|
|
})
|
|
.bind( "keypress.autocomplete", function( event ) {
|
|
if ( suppressKeyPress ) {
|
|
suppressKeyPress = false;
|
|
event.preventDefault();
|
|
}
|
|
})
|
|
.bind( "focus.autocomplete", function() {
|
|
if ( self.options.disabled ) {
|
|
return;
|
|
}
|
|
|
|
self.selectedItem = null;
|
|
self.previous = self.element.val();
|
|
})
|
|
.bind( "blur.autocomplete", function( event ) {
|
|
if ( self.options.disabled ) {
|
|
return;
|
|
}
|
|
|
|
clearTimeout( self.searching );
|
|
// clicks on the menu (or a button to trigger a search) will cause a blur event
|
|
self.closing = setTimeout(function() {
|
|
self.close( event );
|
|
self._change( event );
|
|
}, 150 );
|
|
});
|
|
this._initSource();
|
|
this.response = function() {
|
|
return self._response.apply( self, arguments );
|
|
};
|
|
this.menu = $( "<ul></ul>" )
|
|
.addClass( "ui-autocomplete" )
|
|
.appendTo( $( this.options.appendTo || "body", doc )[0] )
|
|
// prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
|
|
.mousedown(function( event ) {
|
|
// clicking on the scrollbar causes focus to shift to the body
|
|
// but we can't detect a mouseup or a click immediately afterward
|
|
// so we have to track the next mousedown and close the menu if
|
|
// the user clicks somewhere outside of the autocomplete
|
|
var menuElement = self.menu.element[ 0 ];
|
|
if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
|
|
setTimeout(function() {
|
|
$( document ).one( 'mousedown', function( event ) {
|
|
if ( event.target !== self.element[ 0 ] &&
|
|
event.target !== menuElement &&
|
|
!$.ui.contains( menuElement, event.target ) ) {
|
|
self.close();
|
|
}
|
|
});
|
|
}, 1 );
|
|
}
|
|
|
|
// use another timeout to make sure the blur-event-handler on the input was already triggered
|
|
setTimeout(function() {
|
|
clearTimeout( self.closing );
|
|
}, 13);
|
|
})
|
|
.menu({
|
|
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.element.val( item.value );
|
|
}
|
|
}
|
|
},
|
|
selected: function( event, ui ) {
|
|
var item = ui.item.data( "item.autocomplete" ),
|
|
previous = self.previous;
|
|
|
|
// only trigger when focus was lost (click on menu)
|
|
if ( self.element[0] !== doc.activeElement ) {
|
|
self.element.focus();
|
|
self.previous = previous;
|
|
// #6109 - IE triggers two focus events and the second
|
|
// is asynchronous, so we need to reset the previous
|
|
// term synchronously and asynchronously :-(
|
|
setTimeout(function() {
|
|
self.previous = previous;
|
|
self.selectedItem = item;
|
|
}, 1);
|
|
}
|
|
|
|
if ( false !== self._trigger( "select", event, { item: item } ) ) {
|
|
self.element.val( item.value );
|
|
}
|
|
// reset the term after the select event
|
|
// this allows custom select handling to work properly
|
|
self.term = self.element.val();
|
|
|
|
self.close( event );
|
|
self.selectedItem = item;
|
|
},
|
|
blur: function( event, ui ) {
|
|
// 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.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 ) {
|
|
this.menu.element.bgiframe();
|
|
}
|
|
// turning off autocomplete prevents the browser from remembering the
|
|
// value when navigating through history, so we re-enable autocomplete
|
|
// if the page is unloaded before the widget is destroyed. #7790
|
|
self.beforeunloadHandler = function() {
|
|
self.element.removeAttr( "autocomplete" );
|
|
};
|
|
$( window ).bind( "beforeunload", self.beforeunloadHandler );
|
|
},
|
|
|
|
destroy: function() {
|
|
this.element
|
|
.removeClass( "ui-autocomplete-input" )
|
|
.removeAttr( "autocomplete" )
|
|
.removeAttr( "role" )
|
|
.removeAttr( "aria-autocomplete" )
|
|
.removeAttr( "aria-haspopup" );
|
|
this.menu.element.remove();
|
|
$( window ).unbind( "beforeunload", this.beforeunloadHandler );
|
|
$.Widget.prototype.destroy.call( this );
|
|
},
|
|
|
|
_setOption: function( key, value ) {
|
|
$.Widget.prototype._setOption.apply( this, arguments );
|
|
if ( key === "source" ) {
|
|
this._initSource();
|
|
}
|
|
if ( key === "appendTo" ) {
|
|
this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
|
|
}
|
|
if ( key === "disabled" && value && this.xhr ) {
|
|
this.xhr.abort();
|
|
}
|
|
},
|
|
|
|
_initSource: function() {
|
|
var self = this,
|
|
array,
|
|
url;
|
|
if ( $.isArray(this.options.source) ) {
|
|
array = this.options.source;
|
|
this.source = function( request, response ) {
|
|
response( $.ui.autocomplete.filter(array, request.term) );
|
|
};
|
|
} else if ( typeof this.options.source === "string" ) {
|
|
url = this.options.source;
|
|
this.source = function( request, response ) {
|
|
if ( self.xhr ) {
|
|
self.xhr.abort();
|
|
}
|
|
self.xhr = $.ajax({
|
|
url: url,
|
|
data: request,
|
|
dataType: "json",
|
|
context: {
|
|
autocompleteRequest: ++requestIndex
|
|
},
|
|
success: function( data, status ) {
|
|
if ( this.autocompleteRequest === requestIndex ) {
|
|
response( data );
|
|
}
|
|
},
|
|
error: function() {
|
|
if ( this.autocompleteRequest === requestIndex ) {
|
|
response( [] );
|
|
}
|
|
}
|
|
});
|
|
};
|
|
} else {
|
|
this.source = this.options.source;
|
|
}
|
|
},
|
|
|
|
search: function( value, event ) {
|
|
value = value != null ? value : this.element.val();
|
|
|
|
// always save the actual value, not the one passed as an argument
|
|
this.term = this.element.val();
|
|
|
|
if ( value.length < this.options.minLength ) {
|
|
return this.close( event );
|
|
}
|
|
|
|
clearTimeout( this.closing );
|
|
if ( this._trigger( "search", event ) === false ) {
|
|
return;
|
|
}
|
|
|
|
return this._search( value );
|
|
},
|
|
|
|
_search: function( value ) {
|
|
this.pending++;
|
|
this.element.addClass( "ui-autocomplete-loading" );
|
|
|
|
this.source( { term: value }, this.response );
|
|
},
|
|
|
|
_response: function( content ) {
|
|
if ( !this.options.disabled && content && content.length ) {
|
|
content = this._normalize( content );
|
|
this._suggest( content );
|
|
this._trigger( "open" );
|
|
} else {
|
|
this.close();
|
|
}
|
|
this.pending--;
|
|
if ( !this.pending ) {
|
|
this.element.removeClass( "ui-autocomplete-loading" );
|
|
}
|
|
},
|
|
|
|
close: function( event ) {
|
|
clearTimeout( this.closing );
|
|
if ( this.menu.element.is(":visible") ) {
|
|
this.menu.element.hide();
|
|
this.menu.deactivate();
|
|
this._trigger( "close", event );
|
|
}
|
|
},
|
|
|
|
_change: function( event ) {
|
|
if ( this.previous !== this.element.val() ) {
|
|
this._trigger( "change", event, { item: this.selectedItem } );
|
|
}
|
|
},
|
|
|
|
_normalize: function( items ) {
|
|
// assume all items have the right format when the first item is complete
|
|
if ( items.length && items[0].label && items[0].value ) {
|
|
return items;
|
|
}
|
|
return $.map( items, function(item) {
|
|
if ( typeof item === "string" ) {
|
|
return {
|
|
label: item,
|
|
value: item
|
|
};
|
|
}
|
|
return $.extend({
|
|
label: item.label || item.value,
|
|
value: item.value || item.label
|
|
}, item );
|
|
});
|
|
},
|
|
|
|
_suggest: function( items ) {
|
|
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 deactivate
|
|
this.menu.deactivate();
|
|
this.menu.refresh();
|
|
|
|
// size and position menu
|
|
ul.show();
|
|
this._resizeMenu();
|
|
ul.position( $.extend({
|
|
of: this.element
|
|
}, this.options.position ));
|
|
|
|
if ( this.options.autoFocus ) {
|
|
this.menu.next( new $.Event("mouseover") );
|
|
}
|
|
},
|
|
|
|
_resizeMenu: function() {
|
|
var ul = this.menu.element;
|
|
ul.outerWidth( Math.max(
|
|
// Firefox wraps long text (possibly a rounding bug)
|
|
// so we add 1px to avoid the wrapping (#7513)
|
|
ul.width( "" ).outerWidth() + 1,
|
|
this.element.outerWidth()
|
|
) );
|
|
},
|
|
|
|
_renderMenu: function( ul, items ) {
|
|
var self = this;
|
|
$.each( items, function( index, item ) {
|
|
self._renderItem( ul, item );
|
|
});
|
|
},
|
|
|
|
_renderItem: function( ul, item) {
|
|
return $( "<li></li>" )
|
|
.data( "item.autocomplete", item )
|
|
.append( $( "<a></a>" ).text( item.label ) )
|
|
.appendTo( ul );
|
|
},
|
|
|
|
_move: function( direction, event ) {
|
|
if ( !this.menu.element.is(":visible") ) {
|
|
this.search( null, event );
|
|
return;
|
|
}
|
|
if ( this.menu.first() && /^previous/.test(direction) ||
|
|
this.menu.last() && /^next/.test(direction) ) {
|
|
this.element.val( this.term );
|
|
this.menu.deactivate();
|
|
return;
|
|
}
|
|
this.menu[ direction ]( event );
|
|
},
|
|
|
|
widget: function() {
|
|
return this.menu.element;
|
|
}
|
|
});
|
|
|
|
$.extend( $.ui.autocomplete, {
|
|
escapeRegex: function( value ) {
|
|
return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
},
|
|
filter: function(array, term) {
|
|
var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
|
|
return $.grep( array, function(value) {
|
|
return matcher.test( value.label || value.value || value );
|
|
});
|
|
}
|
|
});
|
|
|
|
}( 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.scrollTop(),
|
|
elementHeight = this.element.height();
|
|
if (offset < 0) {
|
|
this.element.scrollTop( scroll + offset);
|
|
} else if (offset >= elementHeight) {
|
|
this.element.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[ $.fn.prop ? "prop" : "attr" ]("scrollHeight");
|
|
},
|
|
|
|
select: function( event ) {
|
|
this._trigger("selected", event, { item: this.active });
|
|
}
|
|
});
|
|
|
|
}(jQuery));
|