Skip to content
This repository
Browse code

initial commit

  • Loading branch information...
commit a6de464f604f67e85befa8e0023b543c0fd9762d 0 parents
Mark Gibson authored
3  .gitignore
... ...
@@ -0,0 +1,3 @@
  1
+dist
  2
+mode
  3
+.svn
3  .gitmodules
... ...
@@ -0,0 +1,3 @@
  1
+[submodule "build"]
  2
+	path = build
  3
+	url = git://github.com/jollytoad/js-common-build.git
7  Makefile
... ...
@@ -0,0 +1,7 @@
  1
+PACKAGE = jquery.ui-tokenlist
  2
+
  3
+MODULES = \
  4
+	jquery.autocomplete.js \
  5
+	ui.tokenlist.js
  6
+
  7
+include build/rules.mk
1  build
... ...
@@ -0,0 +1 @@
  1
+Subproject commit 95f642b2f7691a5972f92b21c68083b3ac8eb995
762  src/jquery.autocomplete.js
... ...
@@ -0,0 +1,762 @@
  1
+/*!
  2
+ * Autocomplete - jQuery plugin 1.0.2
  3
+ *
  4
+ * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
  5
+ *
  6
+ * Dual licensed under the MIT and GPL licenses:
  7
+ *   http://www.opensource.org/licenses/mit-license.php
  8
+ *   http://www.gnu.org/licenses/gpl.html
  9
+ *
  10
+ * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
  11
+ *
  12
+ * With modifications:
  13
+ * 14/01/09 - Mark Gibson - Added ability to filter list before display
  14
+ */
  15
+
  16
+;(function($) {
  17
+	
  18
+$.fn.extend({
  19
+	autocomplete: function(urlOrData, options) {
  20
+		var isUrl = typeof urlOrData == "string";
  21
+		options = $.extend({}, $.Autocompleter.defaults, {
  22
+			url: isUrl ? urlOrData : null,
  23
+			data: isUrl ? null : urlOrData,
  24
+			delay: isUrl ? $.Autocompleter.defaults.delay : 10,
  25
+			max: options && !options.scroll ? 10 : 150
  26
+		}, options);
  27
+		
  28
+		// if highlight is set to false, replace it with a do-nothing function
  29
+		options.highlight = options.highlight || function(value) { return value; };
  30
+		
  31
+		// if the formatMatch option is not specified, then use formatItem for backwards compatibility
  32
+		options.formatMatch = options.formatMatch || options.formatItem;
  33
+		
  34
+		return this.each(function() {
  35
+			new $.Autocompleter(this, options);
  36
+		});
  37
+	},
  38
+	result: function(handler) {
  39
+		return this.bind("result", handler);
  40
+	},
  41
+	search: function(handler) {
  42
+		return this.trigger("search", [handler]);
  43
+	},
  44
+	flushCache: function() {
  45
+		return this.trigger("flushCache");
  46
+	},
  47
+	setOptions: function(options){
  48
+		return this.trigger("setOptions", [options]);
  49
+	},
  50
+	unautocomplete: function() {
  51
+		return this.trigger("unautocomplete");
  52
+	}
  53
+});
  54
+
  55
+$.Autocompleter = function(input, options) {
  56
+
  57
+	var KEY = {
  58
+		UP: 38,
  59
+		DOWN: 40,
  60
+		DEL: 46,
  61
+		TAB: 9,
  62
+		RETURN: 13,
  63
+		ESC: 27,
  64
+		COMMA: 188,
  65
+		PAGEUP: 33,
  66
+		PAGEDOWN: 34,
  67
+		BACKSPACE: 8
  68
+	};
  69
+
  70
+	// Create $ object for input element
  71
+	var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
  72
+
  73
+	var timeout;
  74
+	var previousValue = "";
  75
+	var cache = $.Autocompleter.Cache(options);
  76
+	var hasFocus = 0;
  77
+	var lastKeyPressCode;
  78
+	var config = {
  79
+		mouseDownOnSelect: false
  80
+	};
  81
+	var select = $.Autocompleter.Select(options, input, selectCurrent, config);
  82
+	
  83
+	var blockSubmit;
  84
+	
  85
+	// prevent form submit in opera when selecting with return key
  86
+	$.browser.opera && $(input.form).bind("submit.autocomplete", function() {
  87
+		if (blockSubmit) {
  88
+			blockSubmit = false;
  89
+			return false;
  90
+		}
  91
+	});
  92
+	
  93
+	// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
  94
+	$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
  95
+		// track last key pressed
  96
+		lastKeyPressCode = event.keyCode;
  97
+		switch(event.keyCode) {
  98
+		
  99
+			case KEY.UP:
  100
+				event.preventDefault();
  101
+				if ( select.visible() ) {
  102
+					select.prev();
  103
+				} else {
  104
+					onChange(0, true);
  105
+				}
  106
+				break;
  107
+				
  108
+			case KEY.DOWN:
  109
+				event.preventDefault();
  110
+				if ( select.visible() ) {
  111
+					select.next();
  112
+				} else {
  113
+					onChange(0, true);
  114
+				}
  115
+				break;
  116
+				
  117
+			case KEY.PAGEUP:
  118
+				event.preventDefault();
  119
+				if ( select.visible() ) {
  120
+					select.pageUp();
  121
+				} else {
  122
+					onChange(0, true);
  123
+				}
  124
+				break;
  125
+				
  126
+			case KEY.PAGEDOWN:
  127
+				event.preventDefault();
  128
+				if ( select.visible() ) {
  129
+					select.pageDown();
  130
+				} else {
  131
+					onChange(0, true);
  132
+				}
  133
+				break;
  134
+			
  135
+			// matches also semicolon
  136
+			case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
  137
+			case KEY.TAB:
  138
+			case KEY.RETURN:
  139
+				if( selectCurrent() ) {
  140
+					// stop default to prevent a form submit, Opera needs special handling
  141
+					event.preventDefault();
  142
+					blockSubmit = true;
  143
+					return false;
  144
+				}
  145
+				break;
  146
+				
  147
+			case KEY.ESC:
  148
+				select.hide();
  149
+				break;
  150
+				
  151
+			default:
  152
+				clearTimeout(timeout);
  153
+				timeout = setTimeout(onChange, options.delay);
  154
+				break;
  155
+		}
  156
+	}).focus(function(){
  157
+		// track whether the field has focus, we shouldn't process any
  158
+		// results if the field no longer has focus
  159
+		hasFocus++;
  160
+	}).blur(function() {
  161
+		hasFocus = 0;
  162
+		if (!config.mouseDownOnSelect) {
  163
+			hideResults();
  164
+		}
  165
+	}).click(function() {
  166
+		// show select when clicking in a focused field
  167
+		if ( hasFocus++ > 1 && !select.visible() ) {
  168
+			onChange(0, true);
  169
+		}
  170
+	}).bind("search", function() {
  171
+		// TODO why not just specifying both arguments?
  172
+		var fn = (arguments.length > 1) ? arguments[1] : null;
  173
+		function findValueCallback(q, data) {
  174
+			var result;
  175
+			if( data && data.length ) {
  176
+				for (var i=0; i < data.length; i++) {
  177
+					if( data[i].result.toLowerCase() == q.toLowerCase() ) {
  178
+						result = data[i];
  179
+						break;
  180
+					}
  181
+				}
  182
+			}
  183
+			if( typeof fn == "function" ) fn(result);
  184
+			else $input.trigger("result", result && [result.data, result.value]);
  185
+		}
  186
+		$.each(trimWords($input.val()), function(i, value) {
  187
+			request(value, findValueCallback, findValueCallback);
  188
+		});
  189
+	}).bind("flushCache", function() {
  190
+		cache.flush();
  191
+	}).bind("setOptions", function() {
  192
+		$.extend(options, arguments[1]);
  193
+		// if we've updated the data, repopulate
  194
+		if ( "data" in arguments[1] )
  195
+			cache.populate();
  196
+	}).bind("unautocomplete", function() {
  197
+		select.unbind();
  198
+		$input.unbind();
  199
+		$(input.form).unbind(".autocomplete");
  200
+	});
  201
+	
  202
+	
  203
+	function selectCurrent() {
  204
+		var selected = select.selected();
  205
+		if( !selected )
  206
+			return false;
  207
+		
  208
+		var v = selected.result;
  209
+		previousValue = v;
  210
+		
  211
+		if ( options.multiple ) {
  212
+			var words = trimWords($input.val());
  213
+			if ( words.length > 1 ) {
  214
+				v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
  215
+			}
  216
+			v += options.multipleSeparator;
  217
+		}
  218
+		
  219
+		$input.val(v);
  220
+		hideResultsNow();
  221
+		$input.trigger("result", [selected.data, selected.value]);
  222
+		return true;
  223
+	}
  224
+	
  225
+	function onChange(crap, skipPrevCheck) {
  226
+		if( lastKeyPressCode == KEY.DEL ) {
  227
+			select.hide();
  228
+			return;
  229
+		}
  230
+		
  231
+		var currentValue = $input.val();
  232
+		
  233
+		if ( !skipPrevCheck && currentValue == previousValue )
  234
+			return;
  235
+		
  236
+		previousValue = currentValue;
  237
+		
  238
+		currentValue = lastWord(currentValue);
  239
+		if ( currentValue.length >= options.minChars) {
  240
+			$input.addClass(options.loadingClass);
  241
+			if (!options.matchCase)
  242
+				currentValue = currentValue.toLowerCase();
  243
+			request(currentValue, receiveData, hideResultsNow);
  244
+		} else {
  245
+			stopLoading();
  246
+			select.hide();
  247
+		}
  248
+	};
  249
+	
  250
+	function trimWords(value) {
  251
+		if ( !value ) {
  252
+			return [""];
  253
+		}
  254
+		var words = value.split( options.multipleSeparator );
  255
+		var result = [];
  256
+		$.each(words, function(i, value) {
  257
+			if ( $.trim(value) )
  258
+				result[i] = $.trim(value);
  259
+		});
  260
+		return result;
  261
+	}
  262
+	
  263
+	function lastWord(value) {
  264
+		if ( !options.multiple )
  265
+			return value;
  266
+		var words = trimWords(value);
  267
+		return words[words.length - 1];
  268
+	}
  269
+	
  270
+	// fills in the input box w/the first match (assumed to be the best match)
  271
+	// q: the term entered
  272
+	// sValue: the first matching result
  273
+	function autoFill(q, sValue){
  274
+		// autofill in the complete box w/the first match as long as the user hasn't entered in more data
  275
+		// if the last user key pressed was backspace, don't autofill
  276
+		if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
  277
+			// fill in the value (keep the case the user has typed)
  278
+			$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
  279
+			// select the portion of the value not typed by the user (so the next character will erase)
  280
+			$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
  281
+		}
  282
+	};
  283
+
  284
+	function hideResults() {
  285
+		clearTimeout(timeout);
  286
+		timeout = setTimeout(hideResultsNow, 200);
  287
+	};
  288
+
  289
+	function hideResultsNow() {
  290
+		var wasVisible = select.visible();
  291
+		select.hide();
  292
+		clearTimeout(timeout);
  293
+		stopLoading();
  294
+		if (options.mustMatch) {
  295
+			// call search and run callback
  296
+			$input.search(
  297
+				function (result){
  298
+					// if no value found, clear the input box
  299
+					if( !result ) {
  300
+						if (options.multiple) {
  301
+							var words = trimWords($input.val()).slice(0, -1);
  302
+							$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
  303
+						}
  304
+						else
  305
+							$input.val( "" );
  306
+					}
  307
+				}
  308
+			);
  309
+		}
  310
+		if (wasVisible)
  311
+			// position cursor at end of input field
  312
+			$.Autocompleter.Selection(input, input.value.length, input.value.length);
  313
+	};
  314
+
  315
+	function receiveData(q, data) {
  316
+		if ( data && data.length && hasFocus ) {
  317
+			stopLoading();
  318
+			select.display(data, q);
  319
+			autoFill(q, data[0].value);
  320
+			select.show();
  321
+		} else {
  322
+			hideResultsNow();
  323
+		}
  324
+	};
  325
+
  326
+	function request(term, success, failure) {
  327
+		if (!options.matchCase)
  328
+			term = term.toLowerCase();
  329
+		var data = cache.load(term);
  330
+		// recieve the cached data
  331
+		if (data && data.length) {
  332
+			success(term, data);
  333
+		// if an AJAX url has been supplied, try loading the data now
  334
+		} else if( (typeof options.url == "string") && (options.url.length > 0) ){
  335
+			
  336
+			var extraParams = {
  337
+				timestamp: +new Date()
  338
+			};
  339
+			$.each(options.extraParams, function(key, param) {
  340
+				extraParams[key] = typeof param == "function" ? param() : param;
  341
+			});
  342
+			
  343
+			$.ajax({
  344
+				// try to leverage ajaxQueue plugin to abort previous requests
  345
+				mode: "abort",
  346
+				// limit abortion to this input
  347
+				port: "autocomplete" + input.name,
  348
+				dataType: options.dataType,
  349
+				url: options.url,
  350
+				data: $.extend({
  351
+					q: lastWord(term),
  352
+					limit: options.max
  353
+				}, extraParams),
  354
+				success: function(data) {
  355
+					var parsed = options.parse && options.parse(data) || parse(data);
  356
+					cache.add(term, parsed);
  357
+					success(term, parsed);
  358
+				}
  359
+			});
  360
+		} else {
  361
+			// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
  362
+			select.emptyList();
  363
+			failure(term);
  364
+		}
  365
+	};
  366
+	
  367
+	function parse(data) {
  368
+		var parsed = [];
  369
+		var rows = data.split("\n");
  370
+		for (var i=0; i < rows.length; i++) {
  371
+			var row = $.trim(rows[i]);
  372
+			if (row) {
  373
+				row = row.split("|");
  374
+				parsed[parsed.length] = {
  375
+					data: row,
  376
+					value: row[0],
  377
+					result: options.formatResult && options.formatResult(row, row[0]) || row[0]
  378
+				};
  379
+			}
  380
+		}
  381
+		return parsed;
  382
+	};
  383
+
  384
+	function stopLoading() {
  385
+		$input.removeClass(options.loadingClass);
  386
+	};
  387
+
  388
+};
  389
+
  390
+$.Autocompleter.defaults = {
  391
+	inputClass: "ac_input",
  392
+	resultsClass: "ac_results",
  393
+	loadingClass: "ac_loading",
  394
+	minChars: 1,
  395
+	delay: 400,
  396
+	matchCase: false,
  397
+	matchSubset: true,
  398
+	matchContains: false,
  399
+	cacheLength: 10,
  400
+	max: 100,
  401
+	mustMatch: false,
  402
+	extraParams: {},
  403
+	selectFirst: true,
  404
+	formatItem: function(row) { return row[0]; },
  405
+	formatMatch: null,
  406
+	autoFill: false,
  407
+	width: 0,
  408
+	multiple: false,
  409
+	multipleSeparator: ", ",
  410
+	highlight: function(value, term) {
  411
+		return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
  412
+	},
  413
+    scroll: true,
  414
+    scrollHeight: 180,
  415
+    filter: function(data) { return data; }
  416
+};
  417
+
  418
+$.Autocompleter.Cache = function(options) {
  419
+
  420
+	var data = {};
  421
+	var length = 0;
  422
+	
  423
+	function matchSubset(s, sub) {
  424
+		if (!options.matchCase) 
  425
+			s = s.toLowerCase();
  426
+		var i = s.indexOf(sub);
  427
+		if (i == -1) return false;
  428
+		return i == 0 || options.matchContains;
  429
+	};
  430
+	
  431
+	function add(q, value) {
  432
+		if (length > options.cacheLength){
  433
+			flush();
  434
+		}
  435
+		if (!data[q]){ 
  436
+			length++;
  437
+		}
  438
+		data[q] = value;
  439
+	}
  440
+	
  441
+	function populate(){
  442
+		if( !options.data ) return false;
  443
+		// track the matches
  444
+		var stMatchSets = {},
  445
+			nullData = 0;
  446
+
  447
+		// no url was specified, we need to adjust the cache length to make sure it fits the local data store
  448
+		if( !options.url ) options.cacheLength = 1;
  449
+		
  450
+		// track all options for minChars = 0
  451
+		stMatchSets[""] = [];
  452
+		
  453
+		// loop through the array and create a lookup structure
  454
+		for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
  455
+			var rawValue = options.data[i];
  456
+			// if rawValue is a string, make an array otherwise just reference the array
  457
+			rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
  458
+			
  459
+			var value = options.formatMatch(rawValue, i+1, options.data.length);
  460
+			if ( value === false )
  461
+				continue;
  462
+				
  463
+			var firstChar = value.charAt(0).toLowerCase();
  464
+			// if no lookup array for this character exists, look it up now
  465
+			if( !stMatchSets[firstChar] ) 
  466
+				stMatchSets[firstChar] = [];
  467
+
  468
+			// if the match is a string
  469
+			var row = {
  470
+				value: value,
  471
+				data: rawValue,
  472
+				result: options.formatResult && options.formatResult(rawValue) || value
  473
+			};
  474
+			
  475
+			// push the current match into the set list
  476
+			stMatchSets[firstChar].push(row);
  477
+
  478
+			// keep track of minChars zero items
  479
+			if ( nullData++ < options.max ) {
  480
+				stMatchSets[""].push(row);
  481
+			}
  482
+		};
  483
+
  484
+		// add the data items to the cache
  485
+		$.each(stMatchSets, function(i, value) {
  486
+			// increase the cache size
  487
+			options.cacheLength++;
  488
+			// add to the cache
  489
+			add(i, value);
  490
+		});
  491
+	}
  492
+	
  493
+	// populate any existing data
  494
+	setTimeout(populate, 25);
  495
+	
  496
+	function flush(){
  497
+		data = {};
  498
+		length = 0;
  499
+	}
  500
+	
  501
+	return {
  502
+		flush: flush,
  503
+		add: add,
  504
+		populate: populate,
  505
+		load: function(q) {
  506
+			if (!options.cacheLength || !length)
  507
+				return null;
  508
+			/* 
  509
+			 * if dealing w/local data and matchContains than we must make sure
  510
+			 * to loop through all the data collections looking for matches
  511
+			 */
  512
+			if( !options.url && options.matchContains ){
  513
+				// track all matches
  514
+				var csub = [];
  515
+				// loop through all the data grids for matches
  516
+				for( var k in data ){
  517
+					// don't search through the stMatchSets[""] (minChars: 0) cache
  518
+					// this prevents duplicates
  519
+					if( k.length > 0 ){
  520
+						var c = data[k];
  521
+						$.each(c, function(i, x) {
  522
+							// if we've got a match, add it to the array
  523
+							if (matchSubset(x.value, q)) {
  524
+								csub.push(x);
  525
+							}
  526
+						});
  527
+					}
  528
+				}				
  529
+				return csub;
  530
+			} else 
  531
+			// if the exact item exists, use it
  532
+			if (data[q]){
  533
+				return data[q];
  534
+			} else
  535
+			if (options.matchSubset) {
  536
+				for (var i = q.length - 1; i >= options.minChars; i--) {
  537
+					var c = data[q.substr(0, i)];
  538
+					if (c) {
  539
+						var csub = [];
  540
+						$.each(c, function(i, x) {
  541
+							if (matchSubset(x.value, q)) {
  542
+								csub[csub.length] = x;
  543
+							}
  544
+						});
  545
+						return csub;
  546
+					}
  547
+				}
  548
+			}
  549
+			return null;
  550
+		}
  551
+	};
  552
+};
  553
+
  554
+$.Autocompleter.Select = function (options, input, select, config) {
  555
+	var CLASSES = {
  556
+		ACTIVE: "ac_over"
  557
+	};
  558
+	
  559
+	var listItems,
  560
+		active = -1,
  561
+		data,
  562
+		term = "",
  563
+		needsInit = true,
  564
+		element,
  565
+		list;
  566
+	
  567
+	// Create results
  568
+	function init() {
  569
+		if (!needsInit)
  570
+			return;
  571
+		element = $("<div/>")
  572
+		.hide()
  573
+		.addClass(options.resultsClass)
  574
+		.css("position", "absolute")
  575
+		.appendTo(document.body);
  576
+	
  577
+		list = $("<ul/>").appendTo(element).mouseover( function(event) {
  578
+			if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
  579
+	            active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
  580
+			    $(target(event)).addClass(CLASSES.ACTIVE);            
  581
+	        }
  582
+		}).click(function(event) {
  583
+			$(target(event)).addClass(CLASSES.ACTIVE);
  584
+			select();
  585
+			// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
  586
+			input.focus();
  587
+			return false;
  588
+		}).mousedown(function() {
  589
+			config.mouseDownOnSelect = true;
  590
+		}).mouseup(function() {
  591
+			config.mouseDownOnSelect = false;
  592
+		});
  593
+		
  594
+		if( options.width > 0 )
  595
+			element.css("width", options.width);
  596
+			
  597
+		needsInit = false;
  598
+	} 
  599
+	
  600
+	function target(event) {
  601
+		var element = event.target;
  602
+		while(element && element.tagName != "LI")
  603
+			element = element.parentNode;
  604
+		// more fun with IE, sometimes event.target is empty, just ignore it then
  605
+		if(!element)
  606
+			return [];
  607
+		return element;
  608
+	}
  609
+
  610
+	function moveSelect(step) {
  611
+		listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
  612
+		movePosition(step);
  613
+        var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
  614
+        if(options.scroll) {
  615
+            var offset = 0;
  616
+            listItems.slice(0, active).each(function() {
  617
+				offset += this.offsetHeight;
  618
+			});
  619
+            if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
  620
+                list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
  621
+            } else if(offset < list.scrollTop()) {
  622
+                list.scrollTop(offset);
  623
+            }
  624
+        }
  625
+	};
  626
+	
  627
+	function movePosition(step) {
  628
+		active += step;
  629
+		if (active < 0) {
  630
+			active = listItems.size() - 1;
  631
+		} else if (active >= listItems.size()) {
  632
+			active = 0;
  633
+		}
  634
+	}
  635
+	
  636
+	function limitNumberOfItems(available) {
  637
+		return options.max && options.max < available
  638
+			? options.max
  639
+			: available;
  640
+	}
  641
+	
  642
+	function fillList() {
  643
+		list.empty();
  644
+		var max = limitNumberOfItems(data.length);
  645
+		for (var i=0; i < max; i++) {
  646
+			if (!data[i])
  647
+				continue;
  648
+			var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
  649
+			if ( formatted === false )
  650
+				continue;
  651
+			var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
  652
+			$.data(li, "ac_data", data[i]);
  653
+		}
  654
+		listItems = list.find("li");
  655
+		if ( options.selectFirst ) {
  656
+			listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
  657
+			active = 0;
  658
+		}
  659
+		// apply bgiframe if available
  660
+		if ( $.fn.bgiframe )
  661
+			list.bgiframe();
  662
+	}
  663
+	
  664
+	return {
  665
+		display: function(d, q) {
  666
+			init();
  667
+			data = options.filter.call(input, d, options);
  668
+			term = q;
  669
+			fillList();
  670
+		},
  671
+		next: function() {
  672
+			moveSelect(1);
  673
+		},
  674
+		prev: function() {
  675
+			moveSelect(-1);
  676
+		},
  677
+		pageUp: function() {
  678
+			if (active != 0 && active - 8 < 0) {
  679
+				moveSelect( -active );
  680
+			} else {
  681
+				moveSelect(-8);
  682
+			}
  683
+		},
  684
+		pageDown: function() {
  685
+			if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
  686
+				moveSelect( listItems.size() - 1 - active );
  687
+			} else {
  688
+				moveSelect(8);
  689
+			}
  690
+		},
  691
+		hide: function() {
  692
+			element && element.hide();
  693
+			listItems && listItems.removeClass(CLASSES.ACTIVE);
  694
+			active = -1;
  695
+		},
  696
+		visible : function() {
  697
+			return element && element.is(":visible");
  698
+		},
  699
+		current: function() {
  700
+			return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
  701
+		},
  702
+		show: function() {
  703
+			var offset = $(input).offset();
  704
+			element.css({
  705
+				width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
  706
+				top: offset.top + input.offsetHeight,
  707
+				left: offset.left
  708
+			}).show();
  709
+            if(options.scroll) {
  710
+                list.scrollTop(0);
  711
+                list.css({
  712
+					maxHeight: options.scrollHeight,
  713
+					overflow: 'auto'
  714
+				});
  715
+				
  716
+                if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
  717
+					var listHeight = 0;
  718
+					listItems.each(function() {
  719
+						listHeight += this.offsetHeight;
  720
+					});
  721
+					var scrollbarsVisible = listHeight > options.scrollHeight;
  722
+                    list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
  723
+					if (!scrollbarsVisible) {
  724
+						// IE doesn't recalculate width when scrollbar disappears
  725
+						listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
  726
+					}
  727
+                }
  728
+                
  729
+            }
  730
+		},
  731
+		selected: function() {
  732
+			var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
  733
+			return selected && selected.length && $.data(selected[0], "ac_data");
  734
+		},
  735
+		emptyList: function (){
  736
+			list && list.empty();
  737
+		},
  738
+		unbind: function() {
  739
+			element && element.remove();
  740
+		}
  741
+	};
  742
+};
  743
+
  744
+$.Autocompleter.Selection = function(field, start, end) {
  745
+	if( field.createTextRange ){
  746
+		var selRange = field.createTextRange();
  747
+		selRange.collapse(true);
  748
+		selRange.moveStart("character", start);
  749
+		selRange.moveEnd("character", end);
  750
+		selRange.select();
  751
+	} else if( field.setSelectionRange ){
  752
+		field.setSelectionRange(start, end);
  753
+	} else {
  754
+		if( field.selectionStart ){
  755
+			field.selectionStart = start;
  756
+			field.selectionEnd = end;
  757
+		}
  758
+	}
  759
+	field.focus();
  760
+};
  761
+
  762
+})(jQuery);
274  src/ui.tokenlist.js
... ...
@@ -0,0 +1,274 @@
  1
+/*!
  2
+ * jQuery UI Token List @VERSION
  3
+ *
  4
+ * Copyright (c) 2009 Adaptavist.com
  5
+ * Dual licensed under the MIT and GPL licenses.
  6
+ */
  7
+/* Depends:
  8
+ *  ui.core.js
  9
+ */
  10
+(function($) {
  11
+
  12
+$.widget('ui.tokenlist', {
  13
+
  14
+	_init: function() {
  15
+		var self = this, key = $.ui.keyCode;
  16
+
  17
+		if (this.element.is(':text')) {
  18
+			this.textElem = this.element;
  19
+
  20
+			this.textElem
  21
+				// Hide the original field
  22
+				.hide()
  23
+				// Update our list if the original field is changed
  24
+				.bind('change.' + this.widgetName, function() {
  25
+					self.value(self.textElem.val(), true);
  26
+				});
  27
+
  28
+			// Generate a list element to replace the original field
  29
+			this.element =
  30
+				$('<ul/>')
  31
+					.insertAfter(this.textElem)
  32
+					// Allow the widget to also be accessed via the generated element
  33
+					.data(this.widgetName, this);
  34
+		}
  35
+
  36
+		this.element
  37
+				.addClass(this.widgetBaseClass + ' ui-widget ui-widget-content ui-helper-clearfix')
  38
+
  39
+				.bind('keydown.' + this.widgetName, function(ev) {
  40
+					var focus, disabled = self._getData('disabled');
  41
+
  42
+					switch (ev.keyCode) {
  43
+					case key.LEFT:
  44
+					case key.UP:
  45
+					case key.BACKSPACE:
  46
+						focus = $(ev.target).closest('li').prev('li');
  47
+						break;
  48
+					case key.RIGHT:
  49
+					case key.DOWN:
  50
+					case key.DELETE:
  51
+						focus = $(ev.target).closest('li').next('li.'+self.widgetBaseClass+'-item');
  52
+						if (!focus.length && !disabled) {
  53
+							focus = self.inputElem;
  54
+						}
  55
+						break;
  56
+					case key.HOME:
  57
+					case key.PAGE_UP:
  58
+						focus = $(ev.target).closest('ul').find('>li:first');
  59
+						break;
  60
+					case key.END:
  61
+					case key.PAGE_DOWN:
  62
+						focus = self.inputElem;
  63
+						break;
  64
+					}
  65
+
  66
+					switch (ev.keyCode) {
  67
+					case key.DELETE:
  68
+					case key.BACKSPACE:
  69
+						if (disabled) {
  70
+							focus = undefined;
  71
+						} else {
  72
+							self._removeItem(ev.target);
  73
+						}
  74
+						break;
  75
+					}
  76
+
  77
+					if (focus && focus.length) {
  78
+						focus[0].focus();
  79
+						ev.stopPropagation();
  80
+						ev.preventDefault();
  81
+					}
  82
+				})
  83
+
  84
+				// Delete the item if the button is clicked
  85
+				.bind('click.' + this.widgetName, function(ev) {
  86
+					if (!self._getData('disabled')) {
  87
+						if ($(ev.target).is('.'+self.widgetBaseClass+'-remove')) {
  88
+							self._removeItem(ev.target);
  89
+						}
  90
+						if (this === ev.target) {
  91
+							self.inputElem[0].focus();
  92
+						}
  93
+					}
  94
+				});
  95
+
  96
+		this.inputElem =
  97
+			$('<input type="text"/>')
  98
+				.bind('keydown.' + this.widgetName, function(ev) {
  99
+					if (ev.keyCode === key.LEFT) {
  100
+						// If caret is at the far-left of the field, move focus to the last item
  101
+						var caret;
  102
+						if (this.selectionEnd !== undefined) {
  103
+							caret = this.selectionEnd;
  104
+						}
  105
+						if (caret === 0) {
  106
+							$(this).closest('li').prev('li').each(function() { this.focus(); });
  107
+							ev.preventDefault();
  108
+						}
  109
+					}
  110
+					ev.stopPropagation();
  111
+				})
  112
+				.bind('change.' + this.widgetName, function() {
  113
+					if (self.add($(this).val()).length) {
  114
+						$(this).val('');
  115
+					}
  116
+				});
  117
+
  118
+		// Add the new item input field
  119
+		$('<li/>')
  120
+			.appendTo(this.element)
  121
+			.addClass(this.widgetBaseClass+'-input')
  122
+			.append(this.inputElem);
  123
+
  124
+		if (this.textElem) {
  125
+			this.value(this.textElem.val());
  126
+			
  127
+			if (this.textElem[0].disabled) {
  128
+				this.disable();
  129
+			}
  130
+		} else {
  131
+			this.add(this.items());
  132
+		}
  133
+	},
  134
+
  135
+	_setData: function(key, value) {
  136
+		$.widget.prototype._setData.apply(this, arguments);
  137
+
  138
+		if (key === 'disabled') {
  139
+			this.inputElem[0].disabled = value;
  140
+		}
  141
+	},
  142
+
  143
+	input: function() {
  144
+		return $(this.inputElem);
  145
+	},
  146
+
  147
+	items: function() {
  148
+		return this._getData('items');
  149
+	},
  150
+
  151
+	empty: function() {
  152
+		// Remove all existing items
  153
+		$('> li.'+this.widgetBaseClass+'-item', this.element).remove();
  154
+		this.options.items = [];
  155
+		return this;
  156
+	},
  157
+
  158
+	value: function(newValue, noChange) {
  159
+		var value = this._stringify(this.items());
  160
+
  161
+		if (arguments.length > 0) {
  162
+			var newItems = this._parse(newValue),
  163
+				newValue = this._stringify(newItems);
  164
+
  165
+			if (newValue !== value) {
  166
+				this.empty().add(newItems, noChange);
  167
+				value = newValue;
  168
+			}
  169
+		}
  170
+
  171
+		return value;
  172
+	},
  173
+
  174
+	add: function(newItems, noChange) {
  175
+		var items = this.items(),
  176
+			unique = !this._getData('duplicates'),
  177
+			validate = this._getData('validate'),
  178
+			added = [],
  179
+			self = this;
  180
+
  181
+		if (!$.isArray(newItems)) {
  182
+			newItems = [newItems];
  183
+		}
  184
+
  185
+		$.each(newItems, function(i, item) {
  186
+			// Discard duplicate items if duplicates are not allowed
  187
+			if (unique && $.inArray(item, items) >= 0) { return; }
  188
+
  189
+			// Validate the item
  190
+			if (validate) {
  191
+				if ($.isArray(validate)) {
  192
+					if ($.inArray(item, validate) < 0) { return; }
  193
+				} else if ($.isFunction(validate)) {
  194
+					if (!validate.apply(self, item)) { return; }
  195
+				}
  196
+			}
  197
+
  198
+			added.push(item);
  199
+			items.push(item);
  200
+			self._addItemElem(item);
  201
+		});
  202
+
  203
+		if (added.length && !noChange) {
  204
+			this._change();
  205
+		}
  206
+
  207
+		return added;
  208
+	},
  209
+
  210
+	_addItemElem: function(token) {
  211
+		var input = $('.'+this.widgetBaseClass+'-input', this.element),
  212
+			label =
  213
+				$('<span/>')
  214
+					.addClass(this.widgetBaseClass+'-label')
  215
+					.text(token),
  216
+			button =
  217
+				$('<span>x</span>')
  218
+					.addClass(this.widgetBaseClass+'-remove ui-icon ui-icon-close')
  219
+					.attr('alt', this._getData('removeTip'));
  220
+
  221
+		return $('<li/>')
  222
+			.insertBefore(input)
  223
+			.addClass(this.widgetBaseClass+'-item ui-state-default ui-corner-all')
  224
+			.attr('tabindex','-1')
  225
+			.append(label)
  226
+			.append(button)
  227
+
  228
+			// Apply/remove style for a focused item
  229
+			.bind('focus.' + this.widgetName, function() { $(this).addClass('ui-state-focus'); })
  230
+			.bind('blur.' + this.widgetName, function() { $(this).removeClass('ui-state-focus'); });
  231
+
  232
+			// Fix focusing in IE when clicking within the item
  233
+//			.bind('click', function() { this.focus(); });
  234
+	},
  235
+
  236
+	_removeItem: function(target) {
  237
+		var item = $(target).closest('li');
  238
+		this.items().splice($(item).prevAll('li').length, 1);
  239
+		item.remove();
  240
+		this._change();
  241
+	},
  242
+
  243
+	_parse: function(value) {
  244
+		return (value || '').split(this._getData('split'));
  245
+	},
  246
+
  247
+	_stringify: function(items) {
  248
+		return items.join(this._getData('join'));
  249
+	},
  250
+
  251
+	_change: function() {
  252
+		if (this.textElem) {
  253
+			this.textElem.val(this._stringify(this.items()));
  254
+		}
  255
+		this.element.trigger('change');
  256
+	}
  257
+});
  258
+
  259
+$.extend($.ui.tokenlist, {
  260
+	getter: "add input items value",
  261
+	version: "@VERSION",
  262
+
  263
+	defaults: {
  264
+		split: /\s*,\s*/,
  265
+		join: ', ',
  266
+		removeTip: "Remove Item",
  267
+		duplicates: false,
  268
+		items: [],
  269
+		validate: false // Maybe false, an array of allowed values, or a validation function
  270
+	}
  271
+});
  272
+
  273
+})(jQuery);
  274
+
1  version.txt
... ...
@@ -0,0 +1 @@
  1
+0.1

0 notes on commit a6de464

Please sign in to comment.
Something went wrong with that request. Please try again.