Project

General

Profile

1
/*
2
 * jQuery UI Autocomplete 1.8.6
3
 *
4
 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
5
 * Dual licensed under the MIT or GPL Version 2 licenses.
6
 * http://jquery.org/license
7
 *
8
 * http://docs.jquery.com/UI/Autocomplete
9
 *
10
 * Depends:
11
 *	jquery.ui.core.js
12
 *	jquery.ui.widget.js
13
 *	jquery.ui.position.js
14
 */
15
(function( $, undefined ) {
16

    
17
$.widget( "ui.autocomplete", {
18
	options: {
19
		appendTo: "body",
20
		delay: 300,
21
		minLength: 1,
22
		position: {
23
			my: "left top",
24
			at: "left bottom",
25
			collision: "none"
26
		},
27
		source: null
28
	},
29
	_create: function() {
30
		var self = this,
31
			doc = this.element[ 0 ].ownerDocument,
32
			suppressKeyPress;
33

    
34
		this.element
35
			.addClass( "ui-autocomplete-input" )
36
			.attr( "autocomplete", "off" )
37
			// TODO verify these actually work as intended
38
			.attr({
39
				role: "textbox",
40
				"aria-autocomplete": "list",
41
				"aria-haspopup": "true"
42
			})
43
			.bind( "keydown.autocomplete", function( event ) {
44
				if ( self.options.disabled || self.element.attr( "readonly" ) ) {
45
					return;
46
				}
47

    
48
				suppressKeyPress = false;
49
				var keyCode = $.ui.keyCode;
50
				switch( event.keyCode ) {
51
				case keyCode.PAGE_UP:
52
					self._move( "previousPage", event );
53
					break;
54
				case keyCode.PAGE_DOWN:
55
					self._move( "nextPage", event );
56
					break;
57
				case keyCode.UP:
58
					self._move( "previous", event );
59
					// prevent moving cursor to beginning of text field in some browsers
60
					event.preventDefault();
61
					break;
62
				case keyCode.DOWN:
63
					self._move( "next", event );
64
					// prevent moving cursor to end of text field in some browsers
65
					event.preventDefault();
66
					break;
67
				case keyCode.ENTER:
68
				case keyCode.NUMPAD_ENTER:
69
					// when menu is open and has focus
70
					if ( self.menu.active ) {
71
						// #6055 - Opera still allows the keypress to occur
72
						// which causes forms to submit
73
						suppressKeyPress = true;
74
						event.preventDefault();
75
					}
76
					//passthrough - ENTER and TAB both select the current element
77
				case keyCode.TAB:
78
					if ( !self.menu.active ) {
79
						return;
80
					}
81
					self.menu.select( event );
82
					break;
83
				case keyCode.ESCAPE:
84
					self.element.val( self.term );
85
					self.close( event );
86
					break;
87
				default:
88
					// keypress is triggered before the input value is changed
89
					clearTimeout( self.searching );
90
					self.searching = setTimeout(function() {
91
						// only search if the value has changed
92
						if ( self.term != self.element.val() ) {
93
							self.selectedItem = null;
94
							self.search( null, event );
95
						}
96
					}, self.options.delay );
97
					break;
98
				}
99
			})
100
			.bind( "keypress.autocomplete", function( event ) {
101
				if ( suppressKeyPress ) {
102
					suppressKeyPress = false;
103
					event.preventDefault();
104
				}
105
			})
106
			.bind( "focus.autocomplete", function() {
107
				if ( self.options.disabled ) {
108
					return;
109
				}
110

    
111
				self.selectedItem = null;
112
				self.previous = self.element.val();
113
			})
114
			.bind( "blur.autocomplete", function( event ) {
115
				if ( self.options.disabled ) {
116
					return;
117
				}
118

    
119
				clearTimeout( self.searching );
120
				// clicks on the menu (or a button to trigger a search) will cause a blur event
121
				self.closing = setTimeout(function() {
122
					self.close( event );
123
					self._change( event );
124
				}, 150 );
125
			});
126
		this._initSource();
127
		this.response = function() {
128
			return self._response.apply( self, arguments );
129
		};
130
		this.menu = $( "<ul></ul>" )
131
			.addClass( "ui-autocomplete" )
132
			.appendTo( $( this.options.appendTo || "body", doc )[0] )
133
			// prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
134
			.mousedown(function( event ) {
135
				// clicking on the scrollbar causes focus to shift to the body
136
				// but we can't detect a mouseup or a click immediately afterward
137
				// so we have to track the next mousedown and close the menu if
138
				// the user clicks somewhere outside of the autocomplete
139
				var menuElement = self.menu.element[ 0 ];
140
				if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
141
					setTimeout(function() {
142
						$( document ).one( 'mousedown', function( event ) {
143
							if ( event.target !== self.element[ 0 ] &&
144
								event.target !== menuElement &&
145
								!$.ui.contains( menuElement, event.target ) ) {
146
								self.close();
147
							}
148
						});
149
					}, 1 );
150
				}
151

    
152
				// use another timeout to make sure the blur-event-handler on the input was already triggered
153
				setTimeout(function() {
154
					clearTimeout( self.closing );
155
				}, 13);
156
			})
157
			.menu({
158
				focus: function( event, ui ) {
159
					var item = ui.item.data( "item.autocomplete" );
160
					if ( false !== self._trigger( "focus", event, { item: item } ) ) {
161
						// use value to match what will end up in the input, if it was a key event
162
						if ( /^key/.test(event.originalEvent.type) ) {
163
							self.element.val( item.value );
164
						}
165
					}
166
				},
167
				selected: function( event, ui ) {
168
					var item = ui.item.data( "item.autocomplete" ),
169
						previous = self.previous;
170

    
171
					// only trigger when focus was lost (click on menu)
172
					if ( self.element[0] !== doc.activeElement ) {
173
						self.element.focus();
174
						self.previous = previous;
175
						// #6109 - IE triggers two focus events and the second
176
						// is asynchronous, so we need to reset the previous
177
						// term synchronously and asynchronously :-(
178
						setTimeout(function() {
179
							self.previous = previous;
180
						}, 1);
181
					}
182

    
183
					if ( false !== self._trigger( "select", event, { item: item } ) ) {
184
						self.element.val( item.value );
185
					}
186
					// reset the term after the select event
187
					// this allows custom select handling to work properly
188
					self.term = self.element.val();
189

    
190
					self.close( event );
191
					self.selectedItem = item;
192
				},
193
				blur: function( event, ui ) {
194
					// don't set the value of the text field if it's already correct
195
					// this prevents moving the cursor unnecessarily
196
					if ( self.menu.element.is(":visible") &&
197
						( self.element.val() !== self.term ) ) {
198
						self.element.val( self.term );
199
					}
200
				}
201
			})
202
			.zIndex( this.element.zIndex() + 1 )
203
			// workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
204
			.css({ top: 0, left: 0 })
205
			.hide()
206
			.data( "menu" );
207
		if ( $.fn.bgiframe ) {
208
			 this.menu.element.bgiframe();
209
		}
210
	},
211

    
212
	destroy: function() {
213
		this.element
214
			.removeClass( "ui-autocomplete-input" )
215
			.removeAttr( "autocomplete" )
216
			.removeAttr( "role" )
217
			.removeAttr( "aria-autocomplete" )
218
			.removeAttr( "aria-haspopup" );
219
		this.menu.element.remove();
220
		$.Widget.prototype.destroy.call( this );
221
	},
222

    
223
	_setOption: function( key, value ) {
224
		$.Widget.prototype._setOption.apply( this, arguments );
225
		if ( key === "source" ) {
226
			this._initSource();
227
		}
228
		if ( key === "appendTo" ) {
229
			this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
230
		}
231
	},
232

    
233
	_initSource: function() {
234
		var self = this,
235
			array,
236
			url;
237
		if ( $.isArray(this.options.source) ) {
238
			array = this.options.source;
239
			this.source = function( request, response ) {
240
				response( $.ui.autocomplete.filter(array, request.term) );
241
			};
242
		} else if ( typeof this.options.source === "string" ) {
243
			url = this.options.source;
244
			this.source = function( request, response ) {
245
				if (self.xhr) {
246
					self.xhr.abort();
247
				}
248
				self.xhr = $.getJSON( url, request, function( data, status, xhr ) {
249
					if ( xhr === self.xhr ) {
250
						response( data );
251
					}
252
					self.xhr = null;
253
				});
254
			};
255
		} else {
256
			this.source = this.options.source;
257
		}
258
	},
259

    
260
	search: function( value, event ) {
261
		value = value != null ? value : this.element.val();
262

    
263
		// always save the actual value, not the one passed as an argument
264
		this.term = this.element.val();
265

    
266
		if ( value.length < this.options.minLength ) {
267
			return this.close( event );
268
		}
269

    
270
		clearTimeout( this.closing );
271
		if ( this._trigger( "search", event ) === false ) {
272
			return;
273
		}
274

    
275
		return this._search( value );
276
	},
277

    
278
	_search: function( value ) {
279
		this.element.addClass( "ui-autocomplete-loading" );
280

    
281
		this.source( { term: value }, this.response );
282
	},
283

    
284
	_response: function( content ) {
285
		if ( content && content.length ) {
286
			content = this._normalize( content );
287
			this._suggest( content );
288
			this._trigger( "open" );
289
		} else {
290
			this.close();
291
		}
292
		this.element.removeClass( "ui-autocomplete-loading" );
293
	},
294

    
295
	close: function( event ) {
296
		clearTimeout( this.closing );
297
		if ( this.menu.element.is(":visible") ) {
298
			this._trigger( "close", event );
299
			this.menu.element.hide();
300
			this.menu.deactivate();
301
		}
302
	},
303
	
304
	_change: function( event ) {
305
		if ( this.previous !== this.element.val() ) {
306
			this._trigger( "change", event, { item: this.selectedItem } );
307
		}
308
	},
309

    
310
	_normalize: function( items ) {
311
		// assume all items have the right format when the first item is complete
312
		if ( items.length && items[0].label && items[0].value ) {
313
			return items;
314
		}
315
		return $.map( items, function(item) {
316
			if ( typeof item === "string" ) {
317
				return {
318
					label: item,
319
					value: item
320
				};
321
			}
322
			return $.extend({
323
				label: item.label || item.value,
324
				value: item.value || item.label
325
			}, item );
326
		});
327
	},
328

    
329
	_suggest: function( items ) {
330
		var ul = this.menu.element
331
			.empty()
332
			.zIndex( this.element.zIndex() + 1 );
333
		this._renderMenu( ul, items );
334
		// TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
335
		this.menu.deactivate();
336
		this.menu.refresh();
337
		this.menu.element.show().position( $.extend({
338
			of: this.element
339
		}, this.options.position ));
340

    
341
		this._resizeMenu();
342
	},
343

    
344
	_resizeMenu: function() {
345
		var ul = this.menu.element;
346
		ul.outerWidth( Math.max(
347
			ul.width( "" ).outerWidth(),
348
			this.element.outerWidth()
349
		) );
350
	},
351

    
352
	_renderMenu: function( ul, items ) {
353
		var self = this;
354
		$.each( items, function( index, item ) {
355
			self._renderItem( ul, item );
356
		});
357
	},
358

    
359
	_renderItem: function( ul, item) {
360
		return $( "<li></li>" )
361
			.data( "item.autocomplete", item )
362
			.append( $( "<a></a>" ).text( item.label ) )
363
			.appendTo( ul );
364
	},
365

    
366
	_move: function( direction, event ) {
367
		if ( !this.menu.element.is(":visible") ) {
368
			this.search( null, event );
369
			return;
370
		}
371
		if ( this.menu.first() && /^previous/.test(direction) ||
372
				this.menu.last() && /^next/.test(direction) ) {
373
			this.element.val( this.term );
374
			this.menu.deactivate();
375
			return;
376
		}
377
		this.menu[ direction ]( event );
378
	},
379

    
380
	widget: function() {
381
		return this.menu.element;
382
	}
383
});
384

    
385
$.extend( $.ui.autocomplete, {
386
	escapeRegex: function( value ) {
387
		return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
388
	},
389
	filter: function(array, term) {
390
		var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
391
		return $.grep( array, function(value) {
392
			return matcher.test( value.label || value.value || value );
393
		});
394
	}
395
});
396

    
397
}( jQuery ));
398

    
399
/*
400
 * jQuery UI Menu (not officially released)
401
 * 
402
 * This widget isn't yet finished and the API is subject to change. We plan to finish
403
 * it for the next release. You're welcome to give it a try anyway and give us feedback,
404
 * as long as you're okay with migrating your code later on. We can help with that, too.
405
 *
406
 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
407
 * Dual licensed under the MIT or GPL Version 2 licenses.
408
 * http://jquery.org/license
409
 *
410
 * http://docs.jquery.com/UI/Menu
411
 *
412
 * Depends:
413
 *	jquery.ui.core.js
414
 *  jquery.ui.widget.js
415
 */
416
(function($) {
417

    
418
$.widget("ui.menu", {
419
	_create: function() {
420
		var self = this;
421
		this.element
422
			.addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
423
			.attr({
424
				role: "listbox",
425
				"aria-activedescendant": "ui-active-menuitem"
426
			})
427
			.click(function( event ) {
428
				if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
429
					return;
430
				}
431
				// temporary
432
				event.preventDefault();
433
				self.select( event );
434
			});
435
		this.refresh();
436
	},
437
	
438
	refresh: function() {
439
		var self = this;
440

    
441
		// don't refresh list items that are already adapted
442
		var items = this.element.children("li:not(.ui-menu-item):has(a)")
443
			.addClass("ui-menu-item")
444
			.attr("role", "menuitem");
445
		
446
		items.children("a")
447
			.addClass("ui-corner-all")
448
			.attr("tabindex", -1)
449
			// mouseenter doesn't work with event delegation
450
			.mouseenter(function( event ) {
451
				self.activate( event, $(this).parent() );
452
			})
453
			.mouseleave(function() {
454
				self.deactivate();
455
			});
456
	},
457

    
458
	activate: function( event, item ) {
459
		this.deactivate();
460
		if (this.hasScroll()) {
461
			var offset = item.offset().top - this.element.offset().top,
462
				scroll = this.element.attr("scrollTop"),
463
				elementHeight = this.element.height();
464
			if (offset < 0) {
465
				this.element.attr("scrollTop", scroll + offset);
466
			} else if (offset >= elementHeight) {
467
				this.element.attr("scrollTop", scroll + offset - elementHeight + item.height());
468
			}
469
		}
470
		this.active = item.eq(0)
471
			.children("a")
472
				.addClass("ui-state-hover")
473
				.attr("id", "ui-active-menuitem")
474
			.end();
475
		this._trigger("focus", event, { item: item });
476
	},
477

    
478
	deactivate: function() {
479
		if (!this.active) { return; }
480

    
481
		this.active.children("a")
482
			.removeClass("ui-state-hover")
483
			.removeAttr("id");
484
		this._trigger("blur");
485
		this.active = null;
486
	},
487

    
488
	next: function(event) {
489
		this.move("next", ".ui-menu-item:first", event);
490
	},
491

    
492
	previous: function(event) {
493
		this.move("prev", ".ui-menu-item:last", event);
494
	},
495

    
496
	first: function() {
497
		return this.active && !this.active.prevAll(".ui-menu-item").length;
498
	},
499

    
500
	last: function() {
501
		return this.active && !this.active.nextAll(".ui-menu-item").length;
502
	},
503

    
504
	move: function(direction, edge, event) {
505
		if (!this.active) {
506
			this.activate(event, this.element.children(edge));
507
			return;
508
		}
509
		var next = this.active[direction + "All"](".ui-menu-item").eq(0);
510
		if (next.length) {
511
			this.activate(event, next);
512
		} else {
513
			this.activate(event, this.element.children(edge));
514
		}
515
	},
516

    
517
	// TODO merge with previousPage
518
	nextPage: function(event) {
519
		if (this.hasScroll()) {
520
			// TODO merge with no-scroll-else
521
			if (!this.active || this.last()) {
522
				this.activate(event, this.element.children(".ui-menu-item:first"));
523
				return;
524
			}
525
			var base = this.active.offset().top,
526
				height = this.element.height(),
527
				result = this.element.children(".ui-menu-item").filter(function() {
528
					var close = $(this).offset().top - base - height + $(this).height();
529
					// TODO improve approximation
530
					return close < 10 && close > -10;
531
				});
532

    
533
			// TODO try to catch this earlier when scrollTop indicates the last page anyway
534
			if (!result.length) {
535
				result = this.element.children(".ui-menu-item:last");
536
			}
537
			this.activate(event, result);
538
		} else {
539
			this.activate(event, this.element.children(".ui-menu-item")
540
				.filter(!this.active || this.last() ? ":first" : ":last"));
541
		}
542
	},
543

    
544
	// TODO merge with nextPage
545
	previousPage: function(event) {
546
		if (this.hasScroll()) {
547
			// TODO merge with no-scroll-else
548
			if (!this.active || this.first()) {
549
				this.activate(event, this.element.children(".ui-menu-item:last"));
550
				return;
551
			}
552

    
553
			var base = this.active.offset().top,
554
				height = this.element.height();
555
				result = this.element.children(".ui-menu-item").filter(function() {
556
					var close = $(this).offset().top - base + height - $(this).height();
557
					// TODO improve approximation
558
					return close < 10 && close > -10;
559
				});
560

    
561
			// TODO try to catch this earlier when scrollTop indicates the last page anyway
562
			if (!result.length) {
563
				result = this.element.children(".ui-menu-item:first");
564
			}
565
			this.activate(event, result);
566
		} else {
567
			this.activate(event, this.element.children(".ui-menu-item")
568
				.filter(!this.active || this.first() ? ":last" : ":first"));
569
		}
570
	},
571

    
572
	hasScroll: function() {
573
		return this.element.height() < this.element.attr("scrollHeight");
574
	},
575

    
576
	select: function( event ) {
577
		this._trigger("selected", event, { item: this.active });
578
	}
579
});
580

    
581
}(jQuery));
(17-17/32)