angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

autocomplete.js (17607B)


      1 /*!
      2  * jQuery UI Autocomplete 1.12.1
      3  * http://jqueryui.com
      4  *
      5  * Copyright jQuery Foundation and other contributors
      6  * Released under the MIT license.
      7  * http://jquery.org/license
      8  */
      9 
     10 //>>label: Autocomplete
     11 //>>group: Widgets
     12 //>>description: Lists suggested words as the user is typing.
     13 //>>docs: http://api.jqueryui.com/autocomplete/
     14 //>>demos: http://jqueryui.com/autocomplete/
     15 //>>css.structure: ../../themes/base/core.css
     16 //>>css.structure: ../../themes/base/autocomplete.css
     17 //>>css.theme: ../../themes/base/theme.css
     18 
     19 ( function( factory ) {
     20 	if ( typeof define === "function" && define.amd ) {
     21 
     22 		// AMD. Register as an anonymous module.
     23 		define( [
     24 			"jquery",
     25 			"./menu",
     26 			"./core"
     27 		], factory );
     28 	} else {
     29 
     30 		// Browser globals
     31 		factory( jQuery );
     32 	}
     33 }( function( $ ) {
     34 
     35 $.widget( "ui.autocomplete", {
     36 	version: "1.12.1",
     37 	defaultElement: "<input>",
     38 	options: {
     39 		appendTo: null,
     40 		autoFocus: false,
     41 		delay: 300,
     42 		minLength: 1,
     43 		position: {
     44 			my: "left top",
     45 			at: "left bottom",
     46 			collision: "none"
     47 		},
     48 		source: null,
     49 
     50 		// Callbacks
     51 		change: null,
     52 		close: null,
     53 		focus: null,
     54 		open: null,
     55 		response: null,
     56 		search: null,
     57 		select: null
     58 	},
     59 
     60 	requestIndex: 0,
     61 	pending: 0,
     62 
     63 	_create: function() {
     64 
     65 		// Some browsers only repeat keydown events, not keypress events,
     66 		// so we use the suppressKeyPress flag to determine if we've already
     67 		// handled the keydown event. #7269
     68 		// Unfortunately the code for & in keypress is the same as the up arrow,
     69 		// so we use the suppressKeyPressRepeat flag to avoid handling keypress
     70 		// events when we know the keydown event was used to modify the
     71 		// search term. #7799
     72 		var suppressKeyPress, suppressKeyPressRepeat, suppressInput,
     73 			nodeName = this.element[ 0 ].nodeName.toLowerCase(),
     74 			isTextarea = nodeName === "textarea",
     75 			isInput = nodeName === "input";
     76 
     77 		// Textareas are always multi-line
     78 		// Inputs are always single-line, even if inside a contentEditable element
     79 		// IE also treats inputs as contentEditable
     80 		// All other element types are determined by whether or not they're contentEditable
     81 		this.isMultiLine = isTextarea || !isInput && this._isContentEditable( this.element );
     82 
     83 		this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ];
     84 		this.isNewMenu = true;
     85 
     86 		this._addClass( "ui-autocomplete-input" );
     87 		this.element.attr( "autocomplete", "off" );
     88 
     89 		this._on( this.element, {
     90 			keydown: function( event ) {
     91 				if ( this.element.prop( "readOnly" ) ) {
     92 					suppressKeyPress = true;
     93 					suppressInput = true;
     94 					suppressKeyPressRepeat = true;
     95 					return;
     96 				}
     97 
     98 				suppressKeyPress = false;
     99 				suppressInput = false;
    100 				suppressKeyPressRepeat = false;
    101 				var keyCode = $.ui.keyCode;
    102 				switch ( event.keyCode ) {
    103 				case keyCode.PAGE_UP:
    104 					suppressKeyPress = true;
    105 					this._move( "previousPage", event );
    106 					break;
    107 				case keyCode.PAGE_DOWN:
    108 					suppressKeyPress = true;
    109 					this._move( "nextPage", event );
    110 					break;
    111 				case keyCode.UP:
    112 					suppressKeyPress = true;
    113 					this._keyEvent( "previous", event );
    114 					break;
    115 				case keyCode.DOWN:
    116 					suppressKeyPress = true;
    117 					this._keyEvent( "next", event );
    118 					break;
    119 				case keyCode.ENTER:
    120 
    121 					// when menu is open and has focus
    122 					if ( this.menu.active ) {
    123 
    124 						// #6055 - Opera still allows the keypress to occur
    125 						// which causes forms to submit
    126 						suppressKeyPress = true;
    127 						event.preventDefault();
    128 						this.menu.select( event );
    129 					}
    130 					break;
    131 				case keyCode.TAB:
    132 					if ( this.menu.active ) {
    133 						this.menu.select( event );
    134 					}
    135 					break;
    136 				case keyCode.ESCAPE:
    137 					if ( this.menu.element.is( ":visible" ) ) {
    138 						if ( !this.isMultiLine ) {
    139 							this._value( this.term );
    140 						}
    141 						this.close( event );
    142 
    143 						// Different browsers have different default behavior for escape
    144 						// Single press can mean undo or clear
    145 						// Double press in IE means clear the whole form
    146 						event.preventDefault();
    147 					}
    148 					break;
    149 				default:
    150 					suppressKeyPressRepeat = true;
    151 
    152 					// search timeout should be triggered before the input value is changed
    153 					this._searchTimeout( event );
    154 					break;
    155 				}
    156 			},
    157 			keypress: function( event ) {
    158 				if ( suppressKeyPress ) {
    159 					suppressKeyPress = false;
    160 					if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
    161 						event.preventDefault();
    162 					}
    163 					return;
    164 				}
    165 				if ( suppressKeyPressRepeat ) {
    166 					return;
    167 				}
    168 
    169 				// Replicate some key handlers to allow them to repeat in Firefox and Opera
    170 				var keyCode = $.ui.keyCode;
    171 				switch ( event.keyCode ) {
    172 				case keyCode.PAGE_UP:
    173 					this._move( "previousPage", event );
    174 					break;
    175 				case keyCode.PAGE_DOWN:
    176 					this._move( "nextPage", event );
    177 					break;
    178 				case keyCode.UP:
    179 					this._keyEvent( "previous", event );
    180 					break;
    181 				case keyCode.DOWN:
    182 					this._keyEvent( "next", event );
    183 					break;
    184 				}
    185 			},
    186 			input: function( event ) {
    187 				if ( suppressInput ) {
    188 					suppressInput = false;
    189 					event.preventDefault();
    190 					return;
    191 				}
    192 				this._searchTimeout( event );
    193 			},
    194 			focus: function() {
    195 				this.selectedItem = null;
    196 				this.previous = this._value();
    197 			},
    198 			blur: function( event ) {
    199 				if ( this.cancelBlur ) {
    200 					delete this.cancelBlur;
    201 					return;
    202 				}
    203 
    204 				clearTimeout( this.searching );
    205 				this.close( event );
    206 				this._change( event );
    207 			}
    208 		} );
    209 
    210 		this._initSource();
    211 		this.menu = $( "<ul>" )
    212 			.appendTo( this._appendTo() )
    213 			.menu( {
    214 
    215 				// disable ARIA support, the live region takes care of that
    216 				role: null
    217 			} )
    218 			.hide()
    219 			.menu( "instance" );
    220 
    221 		this._addClass( this.menu.element, "ui-autocomplete", "ui-front" );
    222 		this._on( this.menu.element, {
    223 			mousedown: function( event ) {
    224 
    225 				// prevent moving focus out of the text field
    226 				event.preventDefault();
    227 
    228 				// IE doesn't prevent moving focus even with event.preventDefault()
    229 				// so we set a flag to know when we should ignore the blur event
    230 				this.cancelBlur = true;
    231 				this._delay( function() {
    232 					delete this.cancelBlur;
    233 
    234 					// Support: IE 8 only
    235 					// Right clicking a menu item or selecting text from the menu items will
    236 					// result in focus moving out of the input. However, we've already received
    237 					// and ignored the blur event because of the cancelBlur flag set above. So
    238 					// we restore focus to ensure that the menu closes properly based on the user's
    239 					// next actions.
    240 					if ( this.element[ 0 ] !== $.ui.safeActiveElement( this.document[ 0 ] ) ) {
    241 						this.element.trigger( "focus" );
    242 					}
    243 				} );
    244 			},
    245 			menufocus: function( event, ui ) {
    246 				var label, item;
    247 
    248 				// support: Firefox
    249 				// Prevent accidental activation of menu items in Firefox (#7024 #9118)
    250 				if ( this.isNewMenu ) {
    251 					this.isNewMenu = false;
    252 					if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
    253 						this.menu.blur();
    254 
    255 						this.document.one( "mousemove", function() {
    256 							$( event.target ).trigger( event.originalEvent );
    257 						} );
    258 
    259 						return;
    260 					}
    261 				}
    262 
    263 				item = ui.item.data( "ui-autocomplete-item" );
    264 				if ( false !== this._trigger( "focus", event, { item: item } ) ) {
    265 
    266 					// use value to match what will end up in the input, if it was a key event
    267 					if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
    268 						this._value( item.value );
    269 					}
    270 				}
    271 
    272 				// Announce the value in the liveRegion
    273 				label = ui.item.attr( "aria-label" ) || item.value;
    274 				if ( label && $.trim( label ).length ) {
    275 					this.liveRegion.children().hide();
    276 					$( "<div>" ).text( label ).appendTo( this.liveRegion );
    277 				}
    278 			},
    279 			menuselect: function( event, ui ) {
    280 				var item = ui.item.data( "ui-autocomplete-item" ),
    281 					previous = this.previous;
    282 
    283 				// Only trigger when focus was lost (click on menu)
    284 				if ( this.element[ 0 ] !== $.ui.safeActiveElement( this.document[ 0 ] ) ) {
    285 					this.element.trigger( "focus" );
    286 					this.previous = previous;
    287 
    288 					// #6109 - IE triggers two focus events and the second
    289 					// is asynchronous, so we need to reset the previous
    290 					// term synchronously and asynchronously :-(
    291 					this._delay( function() {
    292 						this.previous = previous;
    293 						this.selectedItem = item;
    294 					} );
    295 				}
    296 
    297 				if ( false !== this._trigger( "select", event, { item: item } ) ) {
    298 					this._value( item.value );
    299 				}
    300 
    301 				// reset the term after the select event
    302 				// this allows custom select handling to work properly
    303 				this.term = this._value();
    304 
    305 				this.close( event );
    306 				this.selectedItem = item;
    307 			}
    308 		} );
    309 
    310 		this.liveRegion = $( "<div>", {
    311 			role: "status",
    312 			"aria-live": "assertive",
    313 			"aria-relevant": "additions"
    314 		} )
    315 			.appendTo( this.document[ 0 ].body );
    316 
    317 		this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" );
    318 
    319 		// Turning off autocomplete prevents the browser from remembering the
    320 		// value when navigating through history, so we re-enable autocomplete
    321 		// if the page is unloaded before the widget is destroyed. #7790
    322 		this._on( this.window, {
    323 			beforeunload: function() {
    324 				this.element.removeAttr( "autocomplete" );
    325 			}
    326 		} );
    327 	},
    328 
    329 	_destroy: function() {
    330 		clearTimeout( this.searching );
    331 		this.element.removeAttr( "autocomplete" );
    332 		this.menu.element.remove();
    333 		this.liveRegion.remove();
    334 	},
    335 
    336 	_setOption: function( key, value ) {
    337 		this._super( key, value );
    338 		if ( key === "source" ) {
    339 			this._initSource();
    340 		}
    341 		if ( key === "appendTo" ) {
    342 			this.menu.element.appendTo( this._appendTo() );
    343 		}
    344 		if ( key === "disabled" && value && this.xhr ) {
    345 			this.xhr.abort();
    346 		}
    347 	},
    348 
    349 	_isEventTargetInWidget: function( event ) {
    350 		var menuElement = this.menu.element[ 0 ];
    351 
    352 		return event.target === this.element[ 0 ] ||
    353 			event.target === menuElement ||
    354 			$.contains( menuElement, event.target );
    355 	},
    356 
    357 	_closeOnClickOutside: function( event ) {
    358 		if ( !this._isEventTargetInWidget( event ) ) {
    359 			this.close();
    360 		}
    361 	},
    362 
    363 	_appendTo: function() {
    364 		var element = this.options.appendTo;
    365 
    366 		if ( element ) {
    367 			element = element.jquery || element.nodeType ?
    368 				$( element ) :
    369 				this.document.find( element ).eq( 0 );
    370 		}
    371 
    372 		if ( !element || !element[ 0 ] ) {
    373 			element = this.element.closest( ".ui-front, dialog" );
    374 		}
    375 
    376 		if ( !element.length ) {
    377 			element = this.document[ 0 ].body;
    378 		}
    379 
    380 		return element;
    381 	},
    382 
    383 	_initSource: function() {
    384 		var array, url,
    385 			that = this;
    386 		if ( $.isArray( this.options.source ) ) {
    387 			array = this.options.source;
    388 			this.source = function( request, response ) {
    389 				response( $.ui.autocomplete.filter( array, request.term ) );
    390 			};
    391 		} else if ( typeof this.options.source === "string" ) {
    392 			url = this.options.source;
    393 			this.source = function( request, response ) {
    394 				if ( that.xhr ) {
    395 					that.xhr.abort();
    396 				}
    397 				that.xhr = $.ajax( {
    398 					url: url,
    399 					data: request,
    400 					dataType: "json",
    401 					success: function( data ) {
    402 						response( data );
    403 					},
    404 					error: function() {
    405 						response( [] );
    406 					}
    407 				} );
    408 			};
    409 		} else {
    410 			this.source = this.options.source;
    411 		}
    412 	},
    413 
    414 	_searchTimeout: function( event ) {
    415 		clearTimeout( this.searching );
    416 		this.searching = this._delay( function() {
    417 
    418 			// Search if the value has changed, or if the user retypes the same value (see #7434)
    419 			var equalValues = this.term === this._value(),
    420 				menuVisible = this.menu.element.is( ":visible" ),
    421 				modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
    422 
    423 			if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) {
    424 				this.selectedItem = null;
    425 				this.search( null, event );
    426 			}
    427 		}, this.options.delay );
    428 	},
    429 
    430 	search: function( value, event ) {
    431 		value = value != null ? value : this._value();
    432 
    433 		// Always save the actual value, not the one passed as an argument
    434 		this.term = this._value();
    435 
    436 		if ( value.length < this.options.minLength ) {
    437 			return this.close( event );
    438 		}
    439 
    440 		if ( this._trigger( "search", event ) === false ) {
    441 			return;
    442 		}
    443 
    444 		return this._search( value );
    445 	},
    446 
    447 	_search: function( value ) {
    448 		this.pending++;
    449 		this._addClass( "ui-autocomplete-loading" );
    450 		this.cancelSearch = false;
    451 
    452 		this.source( { term: value }, this._response() );
    453 	},
    454 
    455 	_response: function() {
    456 		var index = ++this.requestIndex;
    457 
    458 		return $.proxy( function( content ) {
    459 			if ( index === this.requestIndex ) {
    460 				this.__response( content );
    461 			}
    462 
    463 			this.pending--;
    464 			if ( !this.pending ) {
    465 				this._removeClass( "ui-autocomplete-loading" );
    466 			}
    467 		}, this );
    468 	},
    469 
    470 	__response: function( content ) {
    471 		if ( content ) {
    472 			content = this._normalize( content );
    473 		}
    474 		this._trigger( "response", null, { content: content } );
    475 		if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
    476 			this._suggest( content );
    477 			this._trigger( "open" );
    478 		} else {
    479 
    480 			// use ._close() instead of .close() so we don't cancel future searches
    481 			this._close();
    482 		}
    483 	},
    484 
    485 	close: function( event ) {
    486 		this.cancelSearch = true;
    487 		this._close( event );
    488 	},
    489 
    490 	_close: function( event ) {
    491 
    492 		// Remove the handler that closes the menu on outside clicks
    493 		this._off( this.document, "mousedown" );
    494 
    495 		if ( this.menu.element.is( ":visible" ) ) {
    496 			this.menu.element.hide();
    497 			this.menu.blur();
    498 			this.isNewMenu = true;
    499 			this._trigger( "close", event );
    500 		}
    501 	},
    502 
    503 	_change: function( event ) {
    504 		if ( this.previous !== this._value() ) {
    505 			this._trigger( "change", event, { item: this.selectedItem } );
    506 		}
    507 	},
    508 
    509 	_normalize: function( items ) {
    510 
    511 		// assume all items have the right format when the first item is complete
    512 		if ( items.length && items[ 0 ].label && items[ 0 ].value ) {
    513 			return items;
    514 		}
    515 		return $.map( items, function( item ) {
    516 			if ( typeof item === "string" ) {
    517 				return {
    518 					label: item,
    519 					value: item
    520 				};
    521 			}
    522 			return $.extend( {}, item, {
    523 				label: item.label || item.value,
    524 				value: item.value || item.label
    525 			} );
    526 		} );
    527 	},
    528 
    529 	_suggest: function( items ) {
    530 		var ul = this.menu.element.empty();
    531 		this._renderMenu( ul, items );
    532 		this.isNewMenu = true;
    533 		this.menu.refresh();
    534 
    535 		// Size and position menu
    536 		ul.show();
    537 		this._resizeMenu();
    538 		ul.position( $.extend( {
    539 			of: this.element
    540 		}, this.options.position ) );
    541 
    542 		if ( this.options.autoFocus ) {
    543 			this.menu.next();
    544 		}
    545 
    546 		// Listen for interactions outside of the widget (#6642)
    547 		this._on( this.document, {
    548 			mousedown: "_closeOnClickOutside"
    549 		} );
    550 	},
    551 
    552 	_resizeMenu: function() {
    553 		var ul = this.menu.element;
    554 		ul.outerWidth( Math.max(
    555 
    556 			// Firefox wraps long text (possibly a rounding bug)
    557 			// so we add 1px to avoid the wrapping (#7513)
    558 			ul.width( "" ).outerWidth() + 1,
    559 			this.element.outerWidth()
    560 		) );
    561 	},
    562 
    563 	_renderMenu: function( ul, items ) {
    564 		var that = this;
    565 		$.each( items, function( index, item ) {
    566 			that._renderItemData( ul, item );
    567 		} );
    568 	},
    569 
    570 	_renderItemData: function( ul, item ) {
    571 		return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
    572 	},
    573 
    574 	_renderItem: function( ul, item ) {
    575 		return $( "<li>" )
    576 			.append( $( "<div>" ).text( item.label ) )
    577 			.appendTo( ul );
    578 	},
    579 
    580 	_move: function( direction, event ) {
    581 		if ( !this.menu.element.is( ":visible" ) ) {
    582 			this.search( null, event );
    583 			return;
    584 		}
    585 		if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
    586 				this.menu.isLastItem() && /^next/.test( direction ) ) {
    587 
    588 			if ( !this.isMultiLine ) {
    589 				this._value( this.term );
    590 			}
    591 
    592 			this.menu.blur();
    593 			return;
    594 		}
    595 		this.menu[ direction ]( event );
    596 	},
    597 
    598 	widget: function() {
    599 		return this.menu.element;
    600 	},
    601 
    602 	_value: function() {
    603 		return this.valueMethod.apply( this.element, arguments );
    604 	},
    605 
    606 	_keyEvent: function( keyEvent, event ) {
    607 		if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
    608 			this._move( keyEvent, event );
    609 
    610 			// Prevents moving cursor to beginning/end of the text field in some browsers
    611 			event.preventDefault();
    612 		}
    613 	},
    614 
    615 	// Support: Chrome <=50
    616 	// We should be able to just use this.element.prop( "isContentEditable" )
    617 	// but hidden elements always report false in Chrome.
    618 	// https://code.google.com/p/chromium/issues/detail?id=313082
    619 	_isContentEditable: function( element ) {
    620 		if ( !element.length ) {
    621 			return false;
    622 		}
    623 
    624 		var editable = element.prop( "contentEditable" );
    625 
    626 		if ( editable === "inherit" ) {
    627 		  return this._isContentEditable( element.parent() );
    628 		}
    629 
    630 		return editable === "true";
    631 	}
    632 } );
    633 
    634 $.extend( $.ui.autocomplete, {
    635 	escapeRegex: function( value ) {
    636 		return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
    637 	},
    638 	filter: function( array, term ) {
    639 		var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" );
    640 		return $.grep( array, function( value ) {
    641 			return matcher.test( value.label || value.value || value );
    642 		} );
    643 	}
    644 } );
    645 
    646 // Live region extension, adding a `messages` option
    647 // NOTE: This is an experimental API. We are still investigating
    648 // a full solution for string manipulation and internationalization.
    649 $.widget( "ui.autocomplete", $.ui.autocomplete, {
    650 	options: {
    651 		messages: {
    652 			noResults: "No search results.",
    653 			results: function( amount ) {
    654 				return amount + ( amount > 1 ? " results are" : " result is" ) +
    655 					" available, use up and down arrow keys to navigate.";
    656 			}
    657 		}
    658 	},
    659 
    660 	__response: function( content ) {
    661 		var message;
    662 		this._superApply( arguments );
    663 		if ( this.options.disabled || this.cancelSearch ) {
    664 			return;
    665 		}
    666 		if ( content && content.length ) {
    667 			message = this.options.messages.results( content.length );
    668 		} else {
    669 			message = this.options.messages.noResults;
    670 		}
    671 		this.liveRegion.children().hide();
    672 		$( "<div>" ).text( message ).appendTo( this.liveRegion );
    673 	}
    674 } );
    675 
    676 return $.ui.autocomplete;
    677 
    678 } ) );