2
0
mirror of https://github.com/devbridge/jQuery-Autocomplete.git synced 2024-11-09 14:50:57 +00:00

Make plugin chainable, change suggestion event handling to improve performance, change suggestions format, update readme, add Jasmine test suites, add license.

This commit is contained in:
Tomas Kirda 2012-12-18 16:07:02 -06:00
parent 8211e43cfd
commit 459719fc02
17 changed files with 5009 additions and 247 deletions

4
.gitignore vendored
View File

@ -1 +1,5 @@
/*.user /*.user
/*.sln
/*.suo
/.idea*
/*.xml

View File

@ -3,8 +3,8 @@
.autocomplete-w1 { position: absolute; top: 0px; left: 0px; } .autocomplete-w1 { position: absolute; top: 0px; left: 0px; }
.autocomplete { border: 1px solid #999; background: #FFF; cursor: default; text-align: left; max-height: 350px; overflow: auto; /* IE6 specific: */ _height: 350px; _margin: 0; _overflow-x: hidden; } .autocomplete { border: 1px solid #999; background: #FFF; cursor: default; text-align: left; max-height: 350px; overflow: auto; /* IE6 specific: */ _height: 350px; _margin: 0; _overflow-x: hidden; }
.autocomplete .selected { background: #F0F0F0; } .autocomplete-selected { background: #F0F0F0; }
.autocomplete div { padding: 2px 5px; white-space: nowrap; overflow: hidden; } .autocomplete div { padding: 2px 5px; white-space: nowrap; overflow: hidden; }
.autocomplete strong { font-weight: normal; color: #3399FF; } .autocomplete strong { font-weight: normal; color: #3399FF; }
#query { font-size: 28px; padding: 10px; border: 1px solid #CCC; display: block; margin: 40px; } input { font-size: 28px; padding: 10px; border: 1px solid #CCC; display: block; margin: 20px 0; }

View File

@ -7,14 +7,24 @@
<body> <body>
<div class="container"> <div class="container">
<h1>Ajax Autocomplete Demo</h1> <h1>Ajax Autocomplete Demo</h1>
<h2>Ajax Lookup</h2>
<p>Type country name in english:</p> <p>Type country name in english:</p>
<div> <div>
<input type="text" name="query" id="query"/> <input type="text" name="country" id="autocomplete-ajax"/>
</div>
<div id="selction-ajax"></div>
<h2>Local Lookup</h2>
<p>Type country name in english:</p>
<div>
<input type="text" name="country" id="autocomplete"/>
</div> </div>
<div id="selection"></div> <div id="selection"></div>
</div> </div>
<script type="text/javascript" src="scripts/jquery-1.8.2.min.js"></script> <script type="text/javascript" src="scripts/jquery-1.8.2.min.js"></script>
<script type="text/javascript" src="scripts/jquery.mockjax.js"></script>
<script type="text/javascript" src="src/jquery.autocomplete.js"></script> <script type="text/javascript" src="src/jquery.autocomplete.js"></script>
<script type="text/javascript" src="scripts/demo.js"></script> <script type="text/javascript" src="scripts/demo.js"></script>
</body> </body>

577
dist/jquery.autocomplete.js vendored Normal file
View File

@ -0,0 +1,577 @@
/**
* Ajax Autocomplete for jQuery, version 1.2
* (c) 2012 Tomas Kirda
*
* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
* For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/
*
* Last Review: 12/18/2012
*/
/*jslint browser: true, white: true, plusplus: true, vars: true */
/*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */
(function ($) {
'use strict';
var utils = (function () {
return {
extend: function (target, source) {
return $.extend(target, source);
},
addEvent: function (element, eventType, handler) {
if (element.addEventListener) {
element.addEventListener(eventType, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + eventType, handler);
} else {
throw new Error('Browser doesn\'t support addEventListener or attachEvent');
}
},
removeEvent: function (element, eventType, handler) {
if (element.removeEventListener) {
element.removeEventListener(eventType, handler, false);
} else if (element.detachEvent) {
element.detachEvent('on' + eventType, handler);
}
},
createNode: function (html) {
var div = document.createElement('div');
div.innerHTML = html;
return div.firstChild;
}
};
}());
function Autocomplete(el, options) {
var that = this,
defaults = {
minChars: 1,
maxHeight: 300,
deferRequestBy: 0,
width: 0,
highlight: true,
params: {},
formatResult: Autocomplete.formatResult,
delimiter: null,
zIndex: 9999,
type: 'GET',
noCache: false,
enforce: false
};
// Shared variables:
that.element = el;
that.el = $(el);
that.suggestions = [];
that.badQueries = [];
that.selectedIndex = -1;
that.currentValue = that.element.value;
that.intervalId = 0;
that.cachedResponse = [];
that.onChangeInterval = null;
that.onChange = null;
that.ignoreValueChange = false;
that.isLocal = false;
that.suggestionsContainer = null;
that.options = defaults;
that.classes = {
selected: 'autocomplete-selected',
suggestion: 'autocomplete-suggestion'
};
// Initialize and set options:
that.initialize();
that.setOptions(options);
}
Autocomplete.utils = utils;
$.Autocomplete = Autocomplete;
Autocomplete.formatResult = function (suggestion, currentValue) {
var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'),
pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')';
return suggestion.value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
};
Autocomplete.prototype = {
killerFn: null,
initialize: function () {
var that = this,
suggestionSelector = '.' + that.classes.suggestion;
// Remove autocomplete attribute to prevent native suggestions:
this.element.setAttribute('autocomplete', 'off');
this.killerFn = function (e) {
if ($(e.target).closest('.autocomplete').length === 0) {
that.killSuggestions();
that.disableKillerFn();
}
};
// Determine suggestions width:
if (!this.options.width || this.options.width === 'auto') {
this.options.width = this.el.outerWidth();
}
this.suggestionsContainer = Autocomplete.utils.createNode('<div class="autocomplete" style="position: absolute; display: none;"></div>');
var container = $(this.suggestionsContainer);
container.appendTo('body').width(this.options.width);
// Listen for mouse over event on suggestions list:
container.on('mouseover', suggestionSelector, function () {
that.activate($(this).data('index'));
});
// Listen for click event on suggestions list:
container.on('click', suggestionSelector, function () {
that.select($(this).data('index'));
});
this.fixPosition();
// Opera does not like keydown:
if (window.opera) {
this.el.on('keypress', function (e) { that.onKeyPress(e); });
} else {
this.el.on('keydown', function (e) { that.onKeyPress(e); });
}
this.el.on('keyup', function (e) { that.onKeyUp(e); });
this.el.on('blur', function () { that.onBlur(); });
this.el.on('focus', function () { that.fixPosition(); });
},
onBlur: function () {
this.enableKillerFn();
},
setOptions: function (suppliedOptions) {
var options = this.options;
utils.extend(options, suppliedOptions);
this.isLocal = $.isArray(options.lookup);
// Transform lookup array if it's string array:
if (this.isLocal && typeof options.lookup[0] === 'string') {
options.lookup = $.map(options.lookup, function (value) {
return { value: value, data: null };
});
}
// Adjust height, width and z-index:
$(this.suggestionsContainer).css({
'max-height': options.maxHeight + 'px',
'width': options.width,
'z-index': options.zIndex
});
},
clearCache: function () {
this.cachedResponse = [];
this.badQueries = [];
},
disable: function () {
this.disabled = true;
},
enable: function () {
this.disabled = false;
},
fixPosition: function () {
var offset = this.el.offset();
$(this.suggestionsContainer).css({
top: (offset.top + this.el.outerHeight()) + 'px',
left: offset.left + 'px'
});
},
enableKillerFn: function () {
var that = this;
$(document).on('click', that.killerFn);
},
disableKillerFn: function () {
var that = this;
$(document).off('click', that.killerFn);
},
killSuggestions: function () {
var that = this;
that.stopKillSuggestions();
that.intervalId = window.setInterval(function () {
that.hide();
that.stopKillSuggestions();
}, 300);
},
stopKillSuggestions: function () {
window.clearInterval(this.intervalId);
},
onKeyPress: function (e) {
// If suggestions are hidden and user presses arrow down, display suggestions:
if (!this.disabled && !this.visible && e.keyCode === 40 && this.currentValue) {
this.suggest();
return;
}
if (this.disabled || !this.visible) {
return;
}
switch (e.keyCode) {
case 27: //KEY_ESC:
this.el.val(this.currentValue);
this.hide();
break;
case 9: //KEY_TAB:
case 13: //KEY_RETURN:
if (this.selectedIndex === -1) {
this.hide();
return;
}
this.select(this.selectedIndex);
if (e.keyCode === 9) {
return;
}
break;
case 38: //KEY_UP:
this.moveUp();
break;
case 40: //KEY_DOWN:
this.moveDown();
break;
default:
return;
}
// Cancel event if function did not return:
e.stopImmediatePropagation();
e.preventDefault();
},
onKeyUp: function (e) {
if (this.disabled) {
return;
}
switch (e.keyCode) {
case 38: //KEY_UP:
case 40: //KEY_DOWN:
return;
}
clearInterval(this.onChangeInterval);
if (this.currentValue !== this.el.val()) {
if (this.options.deferRequestBy > 0) {
// Defer lookup in case when value changes very quickly:
var me = this;
this.onChangeInterval = setInterval(function () {
me.onValueChange();
}, this.options.deferRequestBy);
} else {
this.onValueChange();
}
}
},
onValueChange: function () {
clearInterval(this.onChangeInterval);
this.currentValue = this.element.value;
var q = this.getQuery(this.currentValue);
this.selectedIndex = -1;
if (this.ignoreValueChange) {
this.ignoreValueChange = false;
return;
}
if (q === '' || q.length < this.options.minChars) {
this.hide();
} else {
this.getSuggestions(q);
}
},
getQuery: function (value) {
var delimiter = this.options.delimiter,
parts;
if (!delimiter) {
return $.trim(value);
}
parts = value.split(delimiter);
return $.trim(parts[parts.length - 1]);
},
getSuggestionsLocal: function (q) {
q = q.toLowerCase();
return {
suggestions: $.grep(this.options.lookup, function (suggestion) {
return suggestion.value.toLowerCase().indexOf(q) !== -1;
})
};
},
getSuggestions: function (q) {
var response,
that = this,
options = that.options;
response = that.isLocal ? that.getSuggestionsLocal(q) : that.cachedResponse[q];
if (response && $.isArray(response.suggestions)) {
that.suggestions = response.suggestions;
that.suggest();
} else if (!that.isBadQuery(q)) {
that.options.params.query = q;
$.ajax({
url: options.serviceUrl,
data: options.params,
type: options.type,
dataType: 'text'
}).done(function (txt) {
that.processResponse(txt);
});
}
},
isBadQuery: function (q) {
var badQueries = this.badQueries,
i = badQueries.length;
while (i--) {
if (q.indexOf(badQueries[i]) === 0) {
return true;
}
}
return false;
},
hide: function () {
this.visible = false;
this.selectedIndex = -1;
$(this.suggestionsContainer).hide();
},
suggest: function () {
if (this.suggestions.length === 0) {
this.hide();
return;
}
var len = this.suggestions.length,
formatResults = this.options.formatResult,
value = this.getQuery(this.currentValue),
suggestion,
className = this.classes.suggestion,
classSelected = this.classes.selected,
container = $(this.suggestionsContainer),
html = '',
i;
// Build suggestions inner HTML:
for (i = 0; i < len; i++) {
suggestion = this.suggestions[i];
html += '<div class="' + className + '" data-index="' + i + '">' + formatResults(suggestion, value) + '</div>';
}
container.html(html).show();
this.visible = true;
// Select first value by default:
this.selectedIndex = 0;
container.children().first().addClass(classSelected);
},
processResponse: function (text) {
var response = $.parseJSON(text);
// If suggestions is string array, convert them to supported format:
if (typeof response.suggestions[0] === 'string') {
response.suggestions = $.map(response.suggestions, function (value) {
return { value: value, data: null };
});
}
// Cache results if cache is not disabled:
if (!this.options.noCache) {
this.cachedResponse[response.query] = response;
if (response.suggestions.length === 0) {
this.badQueries.push(response.query);
}
}
// Display suggestions only if returned query matches current value:
if (response.query === this.getQuery(this.currentValue)) {
this.suggestions = response.suggestions;
this.suggest();
}
},
activate: function (index) {
var activeItem,
selected = this.classes.selected,
container = $(this.suggestionsContainer),
children = container.children();
container.children('.' + selected).removeClass(selected);
this.selectedIndex = index;
if (this.selectedIndex !== -1 && children.length > this.selectedIndex) {
activeItem = children.get(this.selectedIndex);
$(activeItem).addClass(selected);
return activeItem;
}
return null;
},
select: function (i) {
var selectedValue = this.suggestions[i];
if (selectedValue) {
this.el.val(selectedValue);
this.ignoreValueChange = true;
this.hide();
this.onSelect(i);
}
},
change: function (i) {
var onChange,
me = this,
selectedValue = this.suggestions[i],
suggestion;
if (selectedValue) {
suggestion = me.suggestions[i];
me.el.val(me.getValue(suggestion.value));
onChange = me.options.onChange;
if ($.isFunction(onChange)) {
onChange(suggestion, me.el);
}
}
},
moveUp: function () {
if (this.selectedIndex === -1) {
return;
}
if (this.selectedIndex === 0) {
$(this.suggestionsContainer).children().first().removeClass(this.classes.selected);
this.selectedIndex = -1;
this.el.val(this.currentValue);
return;
}
this.adjustScroll(this.selectedIndex - 1);
},
moveDown: function () {
if (this.selectedIndex === (this.suggestions.length - 1)) {
return;
}
this.adjustScroll(this.selectedIndex + 1);
},
adjustScroll: function (index) {
var activeItem = this.activate(index),
offsetTop,
upperBound,
lowerBound,
heightDelta = 25;
if (!activeItem) {
return;
}
offsetTop = activeItem.offsetTop;
upperBound = $(this.suggestionsContainer).scrollTop();
lowerBound = upperBound + this.options.maxHeight - heightDelta;
if (offsetTop < upperBound) {
$(this.suggestionsContainer).scrollTop(offsetTop);
} else if (offsetTop > lowerBound) {
$(this.suggestionsContainer).scrollTop(offsetTop - this.options.maxHeight + heightDelta);
}
this.el.val(this.getValue(this.suggestions[index].value));
},
onSelect: function (index) {
var that = this,
onSelectCallback = that.options.onSelect,
suggestion = that.suggestions[index];
that.el.val(that.getValue(suggestion.value));
if ($.isFunction(onSelectCallback)) {
onSelectCallback.call(that.element, suggestion);
}
},
getValue: function (value) {
var that = this,
delimiter = that.options.delimiter,
currentValue,
parts;
if (!delimiter) {
return value;
}
currentValue = that.currentValue;
parts = currentValue.split(delimiter);
if (parts.length === 1) {
return value;
}
return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value;
}
};
// Create chainable jQuery plugin:
$.fn.autocomplete = function (options, args) {
return this.each(function () {
var dataKey = 'autocomplete',
inputElement = $(this),
instance;
if (typeof options === 'string') {
instance = inputElement.data(dataKey);
if (typeof instance[options] === 'function') {
instance[options](args);
}
} else {
instance = new Autocomplete(this, options);
inputElement.data(dataKey, instance);
}
});
};
}(jQuery));

2
dist/jquery.autocomplete.min.js vendored Normal file

File diff suppressed because one or more lines are too long

8
dist/jquery.autocomplete.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

21
license.txt Normal file
View File

@ -0,0 +1,21 @@
Copyright 2012 DevBridge and other contributors
http://www.devbridge.com/projects/autocomplete/jquery/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1 +1,63 @@
# jQuery Autocomplete #Ajax AutoComplete for jQuery
Ajax Autocomplete for jQuery allows you to easily create
autocomplete/autosuggest boxes for text input fields.
##Usage
Html:
<input type="text" name="country" id="autocomplete"/>
Ajax lookup:
$('#autocomplete').autocomplete({
serviceUrl: '/autocomplete/countries',
onSelect: function (suggestion) {
status.html('You selected: ' + suggestion);
}
});
Local lookup (no ajax):
$('#autocomplete').autocomplete({
lookup: countries,
onSelect: function (suggestion) {
status.html('You selected: ' + suggestion);
}
});
##Response Format
Response from the server must be JSON formatted following JavaScript object:
{
query: "Unit",
suggestions: [
{ value: "United Arab Emirates", data: "AE" },
{ value: "United Kingdom", data: "UK" },
{ value: "United States", data: "US" }
]
}
Data can be any value or object. Data object is passed to formatResults function and onSelect callback. Alternatively, if there is no data you can supply just a string array for suggestions:
{
query: "Unit",
suggestions: ["United Arab Emirates", "United Kingdom", "United States"]
}
Important: query value must match original value in the input field, otherwise suggestions will not be displayed.
##License
Ajax Autocomplete for jQuery is freely distributable under the
terms of an MIT-style [license](https://github.com/devbridge/jQuery-Autocomplete/dist/license.txt).
Copyright notice and permission notice shall be included in all
copies or substantial portions of the Software.
##Authors
Tomas Kirda / [@tkirda](https://twitter.com/tkirda)

View File

@ -4,20 +4,50 @@
$(function () { $(function () {
'use strict'; 'use strict';
// Load countries then initialize plugin:
$.ajax({ $.ajax({
url: 'content/countries.txt', url: 'content/countries.txt',
dataType: 'json' dataType: 'json'
}).done(function (data) { }).done(function (source) {
var status = $('#selection'),
countries = $.map(data, function (value) {
return value;
});
$('#query').autocomplete({ var countriesArray = $.map(source, function (value, key) { return { value: value, data: key }; }),
lookup: countries, countries = $.map(source, function (value) { return value; });
onSelect: function (suggestion) {
status.html('You selected: ' + suggestion); // Setup jQuery ajax mock:
$.mockjax({
url: '*',
responseTime: 200,
response: function (settings) {
var query = settings.data.query,
queryLowerCase = query.toLowerCase(),
suggestions = $.grep(countries, function(country) {
return country.toLowerCase().indexOf(queryLowerCase) !== -1;
}),
response = {
query: query,
suggestions: suggestions
};
this.responseText = JSON.stringify(response);
} }
}); });
// Initialize ajax autocomplete:
$('#autocomplete-ajax').autocomplete({
serviceUrl: '/autosuggest/service/url',
onSelect: function(suggestion) {
$('#selction-ajax').html('You selected: ' + suggestion.value + ', ' + suggestion.data);
}
});
// Initialize autocomplete with local lookup:
$('#autocomplete').autocomplete({
lookup: countriesArray,
onSelect: function (suggestion) {
$('#selection').html('You selected: ' + suggestion.value + ', ' + suggestion.data);
}
});
}); });
}); });

523
scripts/jquery.mockjax.js Normal file
View File

@ -0,0 +1,523 @@
/*!
* MockJax - jQuery Plugin to Mock Ajax requests
*
* Version: 1.5.1
* Released:
* Home: http://github.com/appendto/jquery-mockjax
* Author: Jonathan Sharp (http://jdsharp.com)
* License: MIT,GPL
*
* Copyright (c) 2011 appendTo LLC.
* Dual licensed under the MIT or GPL licenses.
* http://appendto.com/open-source-licenses
*/
(function($) {
var _ajax = $.ajax,
mockHandlers = [],
CALLBACK_REGEX = /=\?(&|$)/,
jsc = (new Date()).getTime();
// Parse the given XML string.
function parseXML(xml) {
if ( window['DOMParser'] == undefined && window.ActiveXObject ) {
DOMParser = function() { };
DOMParser.prototype.parseFromString = function( xmlString ) {
var doc = new ActiveXObject('Microsoft.XMLDOM');
doc.async = 'false';
doc.loadXML( xmlString );
return doc;
};
}
try {
var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' );
if ( $.isXMLDoc( xmlDoc ) ) {
var err = $('parsererror', xmlDoc);
if ( err.length == 1 ) {
throw('Error: ' + $(xmlDoc).text() );
}
} else {
throw('Unable to parse XML');
}
} catch( e ) {
var msg = ( e.name == undefined ? e : e.name + ': ' + e.message );
$(document).trigger('xmlParseError', [ msg ]);
return undefined;
}
return xmlDoc;
}
// Trigger a jQuery event
function trigger(s, type, args) {
(s.context ? $(s.context) : $.event).trigger(type, args);
}
// Check if the data field on the mock handler and the request match. This
// can be used to restrict a mock handler to being used only when a certain
// set of data is passed to it.
function isMockDataEqual( mock, live ) {
var identical = false;
// Test for situations where the data is a querystring (not an object)
if (typeof live === 'string') {
// Querystring may be a regex
return $.isFunction( mock.test ) ? mock.test(live) : mock == live;
}
$.each(mock, function(k, v) {
if ( live[k] === undefined ) {
identical = false;
return identical;
} else {
identical = true;
if ( typeof live[k] == 'object' ) {
return isMockDataEqual(mock[k], live[k]);
} else {
if ( $.isFunction( mock[k].test ) ) {
identical = mock[k].test(live[k]);
} else {
identical = ( mock[k] == live[k] );
}
return identical;
}
}
});
return identical;
}
// Check the given handler should mock the given request
function getMockForRequest( handler, requestSettings ) {
// If the mock was registered with a function, let the function decide if we
// want to mock this request
if ( $.isFunction(handler) ) {
return handler( requestSettings );
}
// Inspect the URL of the request and check if the mock handler's url
// matches the url for this ajax request
if ( $.isFunction(handler.url.test) ) {
// The user provided a regex for the url, test it
if ( !handler.url.test( requestSettings.url ) ) {
return null;
}
} else {
// Look for a simple wildcard '*' or a direct URL match
var star = handler.url.indexOf('*');
if (handler.url !== requestSettings.url && star === -1 ||
!new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace('*', '.+')).test(requestSettings.url)) {
return null;
}
}
// Inspect the data submitted in the request (either POST body or GET query string)
if ( handler.data && requestSettings.data ) {
if ( !isMockDataEqual(handler.data, requestSettings.data) ) {
// They're not identical, do not mock this request
return null;
}
}
// Inspect the request type
if ( handler && handler.type &&
handler.type.toLowerCase() != requestSettings.type.toLowerCase() ) {
// The request type doesn't match (GET vs. POST)
return null;
}
return handler;
}
// If logging is enabled, log the mock to the console
function logMock( mockHandler, requestSettings ) {
var c = $.extend({}, $.mockjaxSettings, mockHandler);
if ( c.log && $.isFunction(c.log) ) {
c.log('MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url, $.extend({}, requestSettings));
}
}
// Process the xhr objects send operation
function _xhrSend(mockHandler, requestSettings, origSettings) {
// This is a substitute for < 1.4 which lacks $.proxy
var process = (function(that) {
return function() {
return (function() {
// The request has returned
this.status = mockHandler.status;
this.statusText = mockHandler.statusText;
this.readyState = 4;
// We have an executable function, call it to give
// the mock handler a chance to update it's data
if ( $.isFunction(mockHandler.response) ) {
mockHandler.response(origSettings);
}
// Copy over our mock to our xhr object before passing control back to
// jQuery's onreadystatechange callback
if ( requestSettings.dataType == 'json' && ( typeof mockHandler.responseText == 'object' ) ) {
this.responseText = JSON.stringify(mockHandler.responseText);
} else if ( requestSettings.dataType == 'xml' ) {
if ( typeof mockHandler.responseXML == 'string' ) {
this.responseXML = parseXML(mockHandler.responseXML);
} else {
this.responseXML = mockHandler.responseXML;
}
} else {
this.responseText = mockHandler.responseText;
}
if( typeof mockHandler.status == 'number' || typeof mockHandler.status == 'string' ) {
this.status = mockHandler.status;
}
if( typeof mockHandler.statusText === "string") {
this.statusText = mockHandler.statusText;
}
// jQuery < 1.4 doesn't have onreadystate change for xhr
if ( $.isFunction(this.onreadystatechange) ) {
if( mockHandler.isTimeout) {
this.status = -1;
}
this.onreadystatechange( mockHandler.isTimeout ? 'timeout' : undefined );
} else if ( mockHandler.isTimeout ) {
// Fix for 1.3.2 timeout to keep success from firing.
this.status = -1;
}
}).apply(that);
};
})(this);
if ( mockHandler.proxy ) {
// We're proxying this request and loading in an external file instead
_ajax({
global: false,
url: mockHandler.proxy,
type: mockHandler.proxyType,
data: mockHandler.data,
dataType: requestSettings.dataType === "script" ? "text/plain" : requestSettings.dataType,
complete: function(xhr, txt) {
mockHandler.responseXML = xhr.responseXML;
mockHandler.responseText = xhr.responseText;
mockHandler.status = xhr.status;
mockHandler.statusText = xhr.statusText;
this.responseTimer = setTimeout(process, mockHandler.responseTime || 0);
}
});
} else {
// type == 'POST' || 'GET' || 'DELETE'
if ( requestSettings.async === false ) {
// TODO: Blocking delay
process();
} else {
this.responseTimer = setTimeout(process, mockHandler.responseTime || 50);
}
}
}
// Construct a mocked XHR Object
function xhr(mockHandler, requestSettings, origSettings, origHandler) {
// Extend with our default mockjax settings
mockHandler = $.extend(true, {}, $.mockjaxSettings, mockHandler);
if (typeof mockHandler.headers === 'undefined') {
mockHandler.headers = {};
}
if ( mockHandler.contentType ) {
mockHandler.headers['content-type'] = mockHandler.contentType;
}
return {
status: mockHandler.status,
statusText: mockHandler.statusText,
readyState: 1,
open: function() { },
send: function() {
origHandler.fired = true;
_xhrSend.call(this, mockHandler, requestSettings, origSettings);
},
abort: function() {
clearTimeout(this.responseTimer);
},
setRequestHeader: function(header, value) {
mockHandler.headers[header] = value;
},
getResponseHeader: function(header) {
// 'Last-modified', 'Etag', 'content-type' are all checked by jQuery
if ( mockHandler.headers && mockHandler.headers[header] ) {
// Return arbitrary headers
return mockHandler.headers[header];
} else if ( header.toLowerCase() == 'last-modified' ) {
return mockHandler.lastModified || (new Date()).toString();
} else if ( header.toLowerCase() == 'etag' ) {
return mockHandler.etag || '';
} else if ( header.toLowerCase() == 'content-type' ) {
return mockHandler.contentType || 'text/plain';
}
},
getAllResponseHeaders: function() {
var headers = '';
$.each(mockHandler.headers, function(k, v) {
headers += k + ': ' + v + "\n";
});
return headers;
}
};
}
// Process a JSONP mock request.
function processJsonpMock( requestSettings, mockHandler, origSettings ) {
// Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here
// because there isn't an easy hook for the cross domain script tag of jsonp
processJsonpUrl( requestSettings );
requestSettings.dataType = "json";
if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) {
createJsonpCallback(requestSettings, mockHandler);
// We need to make sure
// that a JSONP style response is executed properly
var rurl = /^(\w+:)?\/\/([^\/?#]+)/,
parts = rurl.exec( requestSettings.url ),
remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
requestSettings.dataType = "script";
if(requestSettings.type.toUpperCase() === "GET" && remote ) {
var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings );
// Check if we are supposed to return a Deferred back to the mock call, or just
// signal success
if(newMockReturn) {
return newMockReturn;
} else {
return true;
}
}
}
return null;
}
// Append the required callback parameter to the end of the request URL, for a JSONP request
function processJsonpUrl( requestSettings ) {
if ( requestSettings.type.toUpperCase() === "GET" ) {
if ( !CALLBACK_REGEX.test( requestSettings.url ) ) {
requestSettings.url += (/\?/.test( requestSettings.url ) ? "&" : "?") +
(requestSettings.jsonp || "callback") + "=?";
}
} else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) {
requestSettings.data = (requestSettings.data ? requestSettings.data + "&" : "") + (requestSettings.jsonp || "callback") + "=?";
}
}
// Process a JSONP request by evaluating the mocked response text
function processJsonpRequest( requestSettings, mockHandler, origSettings ) {
// Synthesize the mock request for adding a script tag
var callbackContext = origSettings && origSettings.context || requestSettings,
newMock = null;
// If the response handler on the moock is a function, call it
if ( mockHandler.response && $.isFunction(mockHandler.response) ) {
mockHandler.response(origSettings);
} else {
// Evaluate the responseText javascript in a global context
if( typeof mockHandler.responseText === 'object' ) {
$.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')');
} else {
$.globalEval( '(' + mockHandler.responseText + ')');
}
}
// Successful response
jsonpSuccess( requestSettings, mockHandler );
jsonpComplete( requestSettings, mockHandler );
// If we are running under jQuery 1.5+, return a deferred object
if($.Deferred){
newMock = new $.Deferred();
if(typeof mockHandler.responseText == "object"){
newMock.resolveWith( callbackContext, [mockHandler.responseText] );
}
else{
newMock.resolveWith( callbackContext, [$.parseJSON( mockHandler.responseText )] );
}
}
return newMock;
}
// Create the required JSONP callback function for the request
function createJsonpCallback( requestSettings, mockHandler ) {
jsonp = requestSettings.jsonpCallback || ("jsonp" + jsc++);
// Replace the =? sequence both in the query string and the data
if ( requestSettings.data ) {
requestSettings.data = (requestSettings.data + "").replace(CALLBACK_REGEX, "=" + jsonp + "$1");
}
requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, "=" + jsonp + "$1");
// Handle JSONP-style loading
window[ jsonp ] = window[ jsonp ] || function( tmp ) {
data = tmp;
jsonpSuccess( requestSettings, mockHandler );
jsonpComplete( requestSettings, mockHandler );
// Garbage collect
window[ jsonp ] = undefined;
try {
delete window[ jsonp ];
} catch(e) {}
if ( head ) {
head.removeChild( script );
}
};
}
// The JSONP request was successful
function jsonpSuccess(requestSettings, mockHandler) {
// If a local callback was specified, fire it and pass it the data
if ( requestSettings.success ) {
requestSettings.success.call( callbackContext, ( mockHandler.response ? mockHandler.response.toString() : mockHandler.responseText || ''), status, {} );
}
// Fire the global callback
if ( requestSettings.global ) {
trigger(requestSettings, "ajaxSuccess", [{}, requestSettings] );
}
}
// The JSONP request was completed
function jsonpComplete(requestSettings, mockHandler) {
// Process result
if ( requestSettings.complete ) {
requestSettings.complete.call( callbackContext, {} , status );
}
// The request was completed
if ( requestSettings.global ) {
trigger( "ajaxComplete", [{}, requestSettings] );
}
// Handle the global AJAX counter
if ( requestSettings.global && ! --$.active ) {
$.event.trigger( "ajaxStop" );
}
}
// The core $.ajax replacement.
function handleAjax( url, origSettings ) {
var mockRequest, requestSettings, mockHandler;
// If url is an object, simulate pre-1.5 signature
if ( typeof url === "object" ) {
origSettings = url;
url = undefined;
} else {
// work around to support 1.5 signature
origSettings.url = url;
}
// Extend the original settings for the request
requestSettings = $.extend(true, {}, $.ajaxSettings, origSettings);
// Iterate over our mock handlers (in registration order) until we find
// one that is willing to intercept the request
for(var k = 0; k < mockHandlers.length; k++) {
if ( !mockHandlers[k] ) {
continue;
}
mockHandler = getMockForRequest( mockHandlers[k], requestSettings );
if(!mockHandler) {
// No valid mock found for this request
continue;
}
// Handle console logging
logMock( mockHandler, requestSettings );
if ( requestSettings.dataType === "jsonp" ) {
if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) {
// This mock will handle the JSONP request
return mockRequest;
}
}
// Removed to fix #54 - keep the mocking data object intact
//mockHandler.data = requestSettings.data;
mockHandler.cache = requestSettings.cache;
mockHandler.timeout = requestSettings.timeout;
mockHandler.global = requestSettings.global;
(function(mockHandler, requestSettings, origSettings, origHandler) {
mockRequest = _ajax.call($, $.extend(true, {}, origSettings, {
// Mock the XHR object
xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ) }
}));
})(mockHandler, requestSettings, origSettings, mockHandlers[k]);
return mockRequest;
}
// We don't have a mock request, trigger a normal request
return _ajax.apply($, [origSettings]);
}
// Public
$.extend({
ajax: handleAjax
});
$.mockjaxSettings = {
//url: null,
//type: 'GET',
log: function( msg ) {
if ( window[ 'console' ] && window.console.log ) {
window.console.log.apply( console, arguments );
}
},
status: 200,
statusText: "OK",
responseTime: 500,
isTimeout: false,
contentType: 'text/plain',
response: '',
responseText: '',
responseXML: '',
proxy: '',
proxyType: 'GET',
lastModified: null,
etag: '',
headers: {
etag: 'IJF@H#@923uf8023hFO@I#H#',
'content-type' : 'text/plain'
}
};
$.mockjax = function(settings) {
var i = mockHandlers.length;
mockHandlers[i] = settings;
return i;
};
$.mockjaxClear = function(i) {
if ( arguments.length == 1 ) {
mockHandlers[i] = null;
} else {
mockHandlers = [];
}
};
$.mockjax.handler = function(i) {
if ( arguments.length == 1 ) {
return mockHandlers[i];
}
};
})(jQuery);

View File

@ -0,0 +1,60 @@
/*jslint vars: true*/
/*global describe, it, expect, $*/
describe('Autocomplete', function () {
'use strict';
it('Should initialize autocomplete options', function () {
var input = document.createElement('input'),
options = { serviceUrl: '/autocomplete/service/url' },
autocomplete = new $.Autocomplete(input, options);
expect(autocomplete.options.serviceUrl).toEqual(options.serviceUrl);
expect(autocomplete.suggestionsContainer).not.toBeNull();
});
it('Should set autocomplete attribute to "off"', function () {
var input = document.createElement('input'),
autocomplete = new $.Autocomplete(input, {});
expect(autocomplete).not.toBeNull();
expect(input.getAttribute('autocomplete')).toEqual('off');
});
it('Should get current value', function () {
var input = document.createElement('input'),
autocomplete = new $.Autocomplete(input, {
lookup: [{ value: 'Jamaica', data: 'B' }]
});
input.value = 'Jam';
autocomplete.onValueChange();
expect(autocomplete.visible).toBe(true);
expect(autocomplete.currentValue).toEqual('Jam');
});
it('Verify onSelect callback', function () {
var input = document.createElement('input'),
context,
value,
data,
autocomplete = new $.Autocomplete(input, {
lookup: [{ value: 'A', data: 'B' }],
onSelect: function (suggestion) {
context = this;
value = suggestion.value;
data = suggestion.data;
}
});
input.value = 'A';
autocomplete.onValueChange();
autocomplete.select(0);
expect(context).toEqual(input);
expect(value).toEqual('A');
expect(data).toEqual('B');
});
});

View File

@ -0,0 +1,20 @@
Copyright (c) 2008-2011 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,681 @@
jasmine.HtmlReporterHelpers = {};
jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) {
var el = document.createElement(type);
for (var i = 2; i < arguments.length; i++) {
var child = arguments[i];
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else {
if (child) {
el.appendChild(child);
}
}
}
for (var attr in attrs) {
if (attr == "className") {
el[attr] = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr]);
}
}
return el;
};
jasmine.HtmlReporterHelpers.getSpecStatus = function(child) {
var results = child.results();
var status = results.passed() ? 'passed' : 'failed';
if (results.skipped) {
status = 'skipped';
}
return status;
};
jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) {
var parentDiv = this.dom.summary;
var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite';
var parent = child[parentSuite];
if (parent) {
if (typeof this.views.suites[parent.id] == 'undefined') {
this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views);
}
parentDiv = this.views.suites[parent.id].element;
}
parentDiv.appendChild(childElement);
};
jasmine.HtmlReporterHelpers.addHelpers = function(ctor) {
for(var fn in jasmine.HtmlReporterHelpers) {
ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn];
}
};
jasmine.HtmlReporter = function(_doc) {
var self = this;
var doc = _doc || window.document;
var reporterView;
var dom = {};
// Jasmine Reporter Public Interface
self.logRunningSpecs = false;
self.reportRunnerStarting = function(runner) {
var specs = runner.specs() || [];
if (specs.length == 0) {
return;
}
createReporterDom(runner.env.versionString());
doc.body.appendChild(dom.reporter);
setExceptionHandling();
reporterView = new jasmine.HtmlReporter.ReporterView(dom);
reporterView.addSpecs(specs, self.specFilter);
};
self.reportRunnerResults = function(runner) {
reporterView && reporterView.complete();
};
self.reportSuiteResults = function(suite) {
reporterView.suiteComplete(suite);
};
self.reportSpecStarting = function(spec) {
if (self.logRunningSpecs) {
self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
}
};
self.reportSpecResults = function(spec) {
reporterView.specComplete(spec);
};
self.log = function() {
var console = jasmine.getGlobal().console;
if (console && console.log) {
if (console.log.apply) {
console.log.apply(console, arguments);
} else {
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
}
}
};
self.specFilter = function(spec) {
if (!focusedSpecName()) {
return true;
}
return spec.getFullName().indexOf(focusedSpecName()) === 0;
};
return self;
function focusedSpecName() {
var specName;
(function memoizeFocusedSpec() {
if (specName) {
return;
}
var paramMap = [];
var params = jasmine.HtmlReporter.parameters(doc);
for (var i = 0; i < params.length; i++) {
var p = params[i].split('=');
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
}
specName = paramMap.spec;
})();
return specName;
}
function createReporterDom(version) {
dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' },
dom.banner = self.createDom('div', { className: 'banner' },
self.createDom('span', { className: 'title' }, "Jasmine "),
self.createDom('span', { className: 'version' }, version)),
dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}),
dom.alert = self.createDom('div', {className: 'alert'},
self.createDom('span', { className: 'exceptions' },
self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'),
self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))),
dom.results = self.createDom('div', {className: 'results'},
dom.summary = self.createDom('div', { className: 'summary' }),
dom.details = self.createDom('div', { id: 'details' }))
);
}
function noTryCatch() {
return window.location.search.match(/catch=false/);
}
function searchWithCatch() {
var params = jasmine.HtmlReporter.parameters(window.document);
var removed = false;
var i = 0;
while (!removed && i < params.length) {
if (params[i].match(/catch=/)) {
params.splice(i, 1);
removed = true;
}
i++;
}
if (jasmine.CATCH_EXCEPTIONS) {
params.push("catch=false");
}
return params.join("&");
}
function setExceptionHandling() {
var chxCatch = document.getElementById('no_try_catch');
if (noTryCatch()) {
chxCatch.setAttribute('checked', true);
jasmine.CATCH_EXCEPTIONS = false;
}
chxCatch.onclick = function() {
window.location.search = searchWithCatch();
};
}
};
jasmine.HtmlReporter.parameters = function(doc) {
var paramStr = doc.location.search.substring(1);
var params = [];
if (paramStr.length > 0) {
params = paramStr.split('&');
}
return params;
}
jasmine.HtmlReporter.sectionLink = function(sectionName) {
var link = '?';
var params = [];
if (sectionName) {
params.push('spec=' + encodeURIComponent(sectionName));
}
if (!jasmine.CATCH_EXCEPTIONS) {
params.push("catch=false");
}
if (params.length > 0) {
link += params.join("&");
}
return link;
};
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);
jasmine.HtmlReporter.ReporterView = function(dom) {
this.startedAt = new Date();
this.runningSpecCount = 0;
this.completeSpecCount = 0;
this.passedCount = 0;
this.failedCount = 0;
this.skippedCount = 0;
this.createResultsMenu = function() {
this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'},
this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'),
' | ',
this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing'));
this.summaryMenuItem.onclick = function() {
dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, '');
};
this.detailsMenuItem.onclick = function() {
showDetails();
};
};
this.addSpecs = function(specs, specFilter) {
this.totalSpecCount = specs.length;
this.views = {
specs: {},
suites: {}
};
for (var i = 0; i < specs.length; i++) {
var spec = specs[i];
this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views);
if (specFilter(spec)) {
this.runningSpecCount++;
}
}
};
this.specComplete = function(spec) {
this.completeSpecCount++;
if (isUndefined(this.views.specs[spec.id])) {
this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom);
}
var specView = this.views.specs[spec.id];
switch (specView.status()) {
case 'passed':
this.passedCount++;
break;
case 'failed':
this.failedCount++;
break;
case 'skipped':
this.skippedCount++;
break;
}
specView.refresh();
this.refresh();
};
this.suiteComplete = function(suite) {
var suiteView = this.views.suites[suite.id];
if (isUndefined(suiteView)) {
return;
}
suiteView.refresh();
};
this.refresh = function() {
if (isUndefined(this.resultsMenu)) {
this.createResultsMenu();
}
// currently running UI
if (isUndefined(this.runningAlert)) {
this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" });
dom.alert.appendChild(this.runningAlert);
}
this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount);
// skipped specs UI
if (isUndefined(this.skippedAlert)) {
this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" });
}
this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
if (this.skippedCount === 1 && isDefined(dom.alert)) {
dom.alert.appendChild(this.skippedAlert);
}
// passing specs UI
if (isUndefined(this.passedAlert)) {
this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" });
}
this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount);
// failing specs UI
if (isUndefined(this.failedAlert)) {
this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"});
}
this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount);
if (this.failedCount === 1 && isDefined(dom.alert)) {
dom.alert.appendChild(this.failedAlert);
dom.alert.appendChild(this.resultsMenu);
}
// summary info
this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount);
this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing";
};
this.complete = function() {
dom.alert.removeChild(this.runningAlert);
this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
if (this.failedCount === 0) {
dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount)));
} else {
showDetails();
}
dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"));
};
return this;
function showDetails() {
if (dom.reporter.className.search(/showDetails/) === -1) {
dom.reporter.className += " showDetails";
}
}
function isUndefined(obj) {
return typeof obj === 'undefined';
}
function isDefined(obj) {
return !isUndefined(obj);
}
function specPluralizedFor(count) {
var str = count + " spec";
if (count > 1) {
str += "s"
}
return str;
}
};
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView);
jasmine.HtmlReporter.SpecView = function(spec, dom, views) {
this.spec = spec;
this.dom = dom;
this.views = views;
this.symbol = this.createDom('li', { className: 'pending' });
this.dom.symbolSummary.appendChild(this.symbol);
this.summary = this.createDom('div', { className: 'specSummary' },
this.createDom('a', {
className: 'description',
href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()),
title: this.spec.getFullName()
}, this.spec.description)
);
this.detail = this.createDom('div', { className: 'specDetail' },
this.createDom('a', {
className: 'description',
href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
title: this.spec.getFullName()
}, this.spec.getFullName())
);
};
jasmine.HtmlReporter.SpecView.prototype.status = function() {
return this.getSpecStatus(this.spec);
};
jasmine.HtmlReporter.SpecView.prototype.refresh = function() {
this.symbol.className = this.status();
switch (this.status()) {
case 'skipped':
break;
case 'passed':
this.appendSummaryToSuiteDiv();
break;
case 'failed':
this.appendSummaryToSuiteDiv();
this.appendFailureDetail();
break;
}
};
jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() {
this.summary.className += ' ' + this.status();
this.appendToSummary(this.spec, this.summary);
};
jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() {
this.detail.className += ' ' + this.status();
var resultItems = this.spec.results().getItems();
var messagesDiv = this.createDom('div', { className: 'messages' });
for (var i = 0; i < resultItems.length; i++) {
var result = resultItems[i];
if (result.type == 'log') {
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
} else if (result.type == 'expect' && result.passed && !result.passed()) {
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
if (result.trace.stack) {
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
}
}
}
if (messagesDiv.childNodes.length > 0) {
this.detail.appendChild(messagesDiv);
this.dom.details.appendChild(this.detail);
}
};
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) {
this.suite = suite;
this.dom = dom;
this.views = views;
this.element = this.createDom('div', { className: 'suite' },
this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description)
);
this.appendToSummary(this.suite, this.element);
};
jasmine.HtmlReporter.SuiteView.prototype.status = function() {
return this.getSpecStatus(this.suite);
};
jasmine.HtmlReporter.SuiteView.prototype.refresh = function() {
this.element.className += " " + this.status();
};
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView);
/* @deprecated Use jasmine.HtmlReporter instead
*/
jasmine.TrivialReporter = function(doc) {
this.document = doc || document;
this.suiteDivs = {};
this.logRunningSpecs = false;
};
jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
var el = document.createElement(type);
for (var i = 2; i < arguments.length; i++) {
var child = arguments[i];
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else {
if (child) { el.appendChild(child); }
}
}
for (var attr in attrs) {
if (attr == "className") {
el[attr] = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr]);
}
}
return el;
};
jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
var showPassed, showSkipped;
this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' },
this.createDom('div', { className: 'banner' },
this.createDom('div', { className: 'logo' },
this.createDom('span', { className: 'title' }, "Jasmine"),
this.createDom('span', { className: 'version' }, runner.env.versionString())),
this.createDom('div', { className: 'options' },
"Show ",
showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
)
),
this.runnerDiv = this.createDom('div', { className: 'runner running' },
this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
);
this.document.body.appendChild(this.outerDiv);
var suites = runner.suites();
for (var i = 0; i < suites.length; i++) {
var suite = suites[i];
var suiteDiv = this.createDom('div', { className: 'suite' },
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
this.suiteDivs[suite.id] = suiteDiv;
var parentDiv = this.outerDiv;
if (suite.parentSuite) {
parentDiv = this.suiteDivs[suite.parentSuite.id];
}
parentDiv.appendChild(suiteDiv);
}
this.startedAt = new Date();
var self = this;
showPassed.onclick = function(evt) {
if (showPassed.checked) {
self.outerDiv.className += ' show-passed';
} else {
self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
}
};
showSkipped.onclick = function(evt) {
if (showSkipped.checked) {
self.outerDiv.className += ' show-skipped';
} else {
self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
}
};
};
jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
var results = runner.results();
var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
this.runnerDiv.setAttribute("class", className);
//do it twice for IE
this.runnerDiv.setAttribute("className", className);
var specs = runner.specs();
var specCount = 0;
for (var i = 0; i < specs.length; i++) {
if (this.specFilter(specs[i])) {
specCount++;
}
}
var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
};
jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
var results = suite.results();
var status = results.passed() ? 'passed' : 'failed';
if (results.totalCount === 0) { // todo: change this to check results.skipped
status = 'skipped';
}
this.suiteDivs[suite.id].className += " " + status;
};
jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
if (this.logRunningSpecs) {
this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
}
};
jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
var results = spec.results();
var status = results.passed() ? 'passed' : 'failed';
if (results.skipped) {
status = 'skipped';
}
var specDiv = this.createDom('div', { className: 'spec ' + status },
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
this.createDom('a', {
className: 'description',
href: '?spec=' + encodeURIComponent(spec.getFullName()),
title: spec.getFullName()
}, spec.description));
var resultItems = results.getItems();
var messagesDiv = this.createDom('div', { className: 'messages' });
for (var i = 0; i < resultItems.length; i++) {
var result = resultItems[i];
if (result.type == 'log') {
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
} else if (result.type == 'expect' && result.passed && !result.passed()) {
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
if (result.trace.stack) {
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
}
}
}
if (messagesDiv.childNodes.length > 0) {
specDiv.appendChild(messagesDiv);
}
this.suiteDivs[spec.suite.id].appendChild(specDiv);
};
jasmine.TrivialReporter.prototype.log = function() {
var console = jasmine.getGlobal().console;
if (console && console.log) {
if (console.log.apply) {
console.log.apply(console, arguments);
} else {
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
}
}
};
jasmine.TrivialReporter.prototype.getLocation = function() {
return this.document.location;
};
jasmine.TrivialReporter.prototype.specFilter = function(spec) {
var paramMap = {};
var params = this.getLocation().search.substring(1).split('&');
for (var i = 0; i < params.length; i++) {
var p = params[i].split('=');
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
}
if (!paramMap.spec) {
return true;
}
return spec.getFullName().indexOf(paramMap.spec) === 0;
};

View File

@ -0,0 +1,82 @@
body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
#HTMLReporter a { text-decoration: none; }
#HTMLReporter a:hover { text-decoration: underline; }
#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; }
#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; }
#HTMLReporter #jasmine_content { position: fixed; right: 100%; }
#HTMLReporter .version { color: #aaaaaa; }
#HTMLReporter .banner { margin-top: 14px; }
#HTMLReporter .duration { color: #aaaaaa; float: right; }
#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; }
#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; }
#HTMLReporter .symbolSummary li.passed { font-size: 14px; }
#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; }
#HTMLReporter .symbolSummary li.failed { line-height: 9px; }
#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
#HTMLReporter .symbolSummary li.skipped { font-size: 14px; }
#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; }
#HTMLReporter .symbolSummary li.pending { line-height: 11px; }
#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; }
#HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; }
#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
#HTMLReporter .runningAlert { background-color: #666666; }
#HTMLReporter .skippedAlert { background-color: #aaaaaa; }
#HTMLReporter .skippedAlert:first-child { background-color: #333333; }
#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; }
#HTMLReporter .passingAlert { background-color: #a6b779; }
#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; }
#HTMLReporter .failingAlert { background-color: #cf867e; }
#HTMLReporter .failingAlert:first-child { background-color: #b03911; }
#HTMLReporter .results { margin-top: 14px; }
#HTMLReporter #details { display: none; }
#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; }
#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
#HTMLReporter.showDetails .summary { display: none; }
#HTMLReporter.showDetails #details { display: block; }
#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
#HTMLReporter .summary { margin-top: 14px; }
#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; }
#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; }
#HTMLReporter .summary .specSummary.failed a { color: #b03911; }
#HTMLReporter .description + .suite { margin-top: 0; }
#HTMLReporter .suite { margin-top: 14px; }
#HTMLReporter .suite a { color: #333333; }
#HTMLReporter #details .specDetail { margin-bottom: 28px; }
#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; }
#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; }
#HTMLReporter .resultMessage span.result { display: block; }
#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }
#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ }
#TrivialReporter a:visited, #TrivialReporter a { color: #303; }
#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; }
#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; }
#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; }
#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; }
#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; }
#TrivialReporter .runner.running { background-color: yellow; }
#TrivialReporter .options { text-align: right; font-size: .8em; }
#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; }
#TrivialReporter .suite .suite { margin: 5px; }
#TrivialReporter .suite.passed { background-color: #dfd; }
#TrivialReporter .suite.failed { background-color: #fdd; }
#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; }
#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; }
#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; }
#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; }
#TrivialReporter .spec.skipped { background-color: #bbb; }
#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; }
#TrivialReporter .passed { background-color: #cfc; display: none; }
#TrivialReporter .failed { background-color: #fbb; }
#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; }
#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; }
#TrivialReporter .resultMessage .mismatch { color: black; }
#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; }
#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; }
#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; }
#TrivialReporter #jasmine_content { position: fixed; right: 100%; }
#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; }

File diff suppressed because it is too large Load Diff

32
spec/runner.html Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Autocomplete Spec</title>
<!-- jasmine -->
<link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css" />
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>
<!-- jQuery -->
<script src="../scripts/jquery-1.8.2.min.js"></script>
<!-- Autocomplete -->
<script src="../src/jquery.autocomplete.js"></script>
<!-- specs -->
<script type="text/javascript" src="autocompleteBehavior.js"></script>
</head>
<body>
<script type="text/javascript">
/*jslint vars: true; */
(function () {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 500;
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = htmlReporter.specFilter;
jasmineEnv.execute();
}());
</script>
</body>
</html>

View File

@ -1,71 +1,104 @@
/** /**
* Ajax Autocomplete for jQuery, version 1.1.5 * Ajax Autocomplete for jQuery, version 1.2
* (c) 2012 Tomas Kirda, Vytautas Pranskunas * (c) 2012 Tomas Kirda
* *
* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
* For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/ * For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/
* *
* Last Review: 11/08/2012 * Last Review: 12/18/2012
*/ */
/*jslint browser: true, white: true, plusplus: true */ /*jslint browser: true, white: true, plusplus: true, vars: true */
/*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */ /*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */
(function ($) { (function ($) {
'use strict'; 'use strict';
var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'); var utils = (function () {
return {
function fnFormatResult(value, data, currentValue) { extend: function (target, source) {
var pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')'; return $.extend(target, source);
return value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>'); },
}
addEvent: function (element, eventType, handler) {
if (element.addEventListener) {
element.addEventListener(eventType, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + eventType, handler);
} else {
throw new Error('Browser doesn\'t support addEventListener or attachEvent');
}
},
removeEvent: function (element, eventType, handler) {
if (element.removeEventListener) {
element.removeEventListener(eventType, handler, false);
} else if (element.detachEvent) {
element.detachEvent('on' + eventType, handler);
}
},
createNode: function (html) {
var div = document.createElement('div');
div.innerHTML = html;
return div.firstChild;
}
};
}());
function Autocomplete(el, options) { function Autocomplete(el, options) {
this.el = $(el); var that = this,
this.el.attr('autocomplete', 'off'); defaults = {
this.suggestions = []; minChars: 1,
this.data = []; maxHeight: 300,
this.badQueries = []; deferRequestBy: 0,
this.selectedIndex = -1; width: 0,
this.currentValue = this.el.val(); highlight: true,
this.intervalId = 0; params: {},
this.cachedResponse = []; formatResult: Autocomplete.formatResult,
this.onChangeInterval = null; delimiter: null,
this.onChange = null; zIndex: 9999,
this.ignoreValueChange = false; type: 'GET',
this.serviceUrl = options.serviceUrl; noCache: false,
this.isLocal = false; enforce: false
this.options = { };
autoSubmit: false,
minChars: 1, // Shared variables:
maxHeight: 300, that.element = el;
deferRequestBy: 0, that.el = $(el);
width: 0, that.suggestions = [];
highlight: true, that.badQueries = [];
params: {}, that.selectedIndex = -1;
fnFormatResult: fnFormatResult, that.currentValue = that.element.value;
delimiter: null, that.intervalId = 0;
zIndex: 9999 that.cachedResponse = [];
that.onChangeInterval = null;
that.onChange = null;
that.ignoreValueChange = false;
that.isLocal = false;
that.suggestionsContainer = null;
that.options = defaults;
that.classes = {
selected: 'autocomplete-selected',
suggestion: 'autocomplete-suggestion'
}; };
this.initialize();
this.setOptions(options); // Initialize and set options:
this.el.data('autocomplete', this); that.initialize();
that.setOptions(options);
} }
$.fn.autocomplete = function (options, args) { Autocomplete.utils = utils;
var instance;
if (typeof options === 'string') { $.Autocomplete = Autocomplete;
instance = this.data('autocomplete');
if (typeof instance[options] === 'function') {
instance[options](args);
}
} else {
instance = new Autocomplete(this.get(0) || $('<input />'), options);
}
return instance; Autocomplete.formatResult = function (suggestion, currentValue) {
var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'),
pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')';
return suggestion.value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
}; };
Autocomplete.prototype = { Autocomplete.prototype = {
@ -73,60 +106,78 @@
killerFn: null, killerFn: null,
initialize: function () { initialize: function () {
var me = this, var that = this,
uid = Math.floor(Math.random() * 0x100000).toString(16), suggestionSelector = '.' + that.classes.suggestion;
autocompleteElId = 'Autocomplete_' + uid,
onKeyPress = function (e) { // Remove autocomplete attribute to prevent native suggestions:
me.onKeyPress(e); this.element.setAttribute('autocomplete', 'off');
};
this.killerFn = function (e) { this.killerFn = function (e) {
if ($(e.target).parents('.autocomplete').size() === 0) { if ($(e.target).closest('.autocomplete').length === 0) {
me.killSuggestions(); that.killSuggestions();
me.disableKillerFn(); that.disableKillerFn();
} }
}; };
if (!this.options.width) { // Determine suggestions width:
if (!this.options.width || this.options.width === 'auto') {
this.options.width = this.el.outerWidth(); this.options.width = this.el.outerWidth();
} }
this.mainContainerId = 'AutocompleteContainter_' + uid; this.suggestionsContainer = Autocomplete.utils.createNode('<div class="autocomplete" style="position: absolute; display: none;"></div>');
$('<div id="' + this.mainContainerId + '" style="position:absolute;z-index:9999;"><div class="autocomplete-w1"><div class="autocomplete" id="' + autocompleteElId + '" style="display:none; width:300px;"></div></div></div>').appendTo('body'); var container = $(this.suggestionsContainer);
container.appendTo('body').width(this.options.width);
// Listen for mouse over event on suggestions list:
container.on('mouseover', suggestionSelector, function () {
that.activate($(this).data('index'));
});
// Listen for click event on suggestions list:
container.on('click', suggestionSelector, function () {
that.select($(this).data('index'));
});
this.container = $('#' + autocompleteElId);
this.fixPosition(); this.fixPosition();
// Opera does not like keydown:
if (window.opera) { if (window.opera) {
this.el.keypress(onKeyPress); this.el.on('keypress', function (e) { that.onKeyPress(e); });
} else { } else {
this.el.keydown(onKeyPress); this.el.on('keydown', function (e) { that.onKeyPress(e); });
} }
this.el.keyup(function (e) { me.onKeyUp(e); }); this.el.on('keyup', function (e) { that.onKeyUp(e); });
this.el.blur(function () { me.enableKillerFn(); }); this.el.on('blur', function () { that.onBlur(); });
this.el.focus(function () { me.fixPosition(); }); this.el.on('focus', function () { that.fixPosition(); });
this.el.change(function () { me.onValueChanged(); });
}, },
extendOptions: function (options) { onBlur: function () {
$.extend(this.options, options); this.enableKillerFn();
}, },
setOptions: function (options) { setOptions: function (suppliedOptions) {
var o = this.options; var options = this.options;
this.extendOptions(options); utils.extend(options, suppliedOptions);
if (o.lookup || o.isLocal) { this.isLocal = $.isArray(options.lookup);
this.isLocal = true;
if ($.isArray(o.lookup)) { o.lookup = { suggestions: o.lookup, data: [] }; } // Transform lookup array if it's string array:
if (this.isLocal && typeof options.lookup[0] === 'string') {
options.lookup = $.map(options.lookup, function (value) {
return { value: value, data: null };
});
} }
$('#' + this.mainContainerId).css({ zIndex: o.zIndex }); // Adjust height, width and z-index:
$(this.suggestionsContainer).css({
this.container.css({ maxHeight: o.maxHeight + 'px', width: o.width }); 'max-height': options.maxHeight + 'px',
'width': options.width,
'z-index': options.zIndex
});
}, },
clearCache: function () { clearCache: function () {
@ -144,25 +195,28 @@
fixPosition: function () { fixPosition: function () {
var offset = this.el.offset(); var offset = this.el.offset();
$('#' + this.mainContainerId).css({ top: (offset.top + this.el.outerHeight()) + 'px', left: offset.left + 'px' }); $(this.suggestionsContainer).css({
top: (offset.top + this.el.outerHeight()) + 'px',
left: offset.left + 'px'
});
}, },
enableKillerFn: function () { enableKillerFn: function () {
var me = this; var that = this;
$(document).bind('click', me.killerFn); $(document).on('click', that.killerFn);
}, },
disableKillerFn: function () { disableKillerFn: function () {
var me = this; var that = this;
$(document).unbind('click', me.killerFn); $(document).off('click', that.killerFn);
}, },
killSuggestions: function () { killSuggestions: function () {
var me = this; var that = this;
me.stopKillSuggestions(); that.stopKillSuggestions();
me.intervalId = window.setInterval(function () { that.intervalId = window.setInterval(function () {
me.hide(); that.hide();
me.stopKillSuggestions(); that.stopKillSuggestions();
}, 300); }, 300);
}, },
@ -170,12 +224,14 @@
window.clearInterval(this.intervalId); window.clearInterval(this.intervalId);
}, },
onValueChanged: function () {
this.change(this.selectedIndex);
},
onKeyPress: function (e) { onKeyPress: function (e) {
if (this.disabled || !this.enabled) { // If suggestions are hidden and user presses arrow down, display suggestions:
if (!this.disabled && !this.visible && e.keyCode === 40 && this.currentValue) {
this.suggest();
return;
}
if (this.disabled || !this.visible) {
return; return;
} }
@ -227,7 +283,9 @@
if (this.options.deferRequestBy > 0) { if (this.options.deferRequestBy > 0) {
// Defer lookup in case when value changes very quickly: // Defer lookup in case when value changes very quickly:
var me = this; var me = this;
this.onChangeInterval = setInterval(function () { me.onValueChange(); }, this.options.deferRequestBy); this.onChangeInterval = setInterval(function () {
me.onValueChange();
}, this.options.deferRequestBy);
} else { } else {
this.onValueChange(); this.onValueChange();
} }
@ -236,7 +294,7 @@
onValueChange: function () { onValueChange: function () {
clearInterval(this.onChangeInterval); clearInterval(this.onChangeInterval);
this.currentValue = this.el.val(); this.currentValue = this.element.value;
var q = this.getQuery(this.currentValue); var q = this.getQuery(this.currentValue);
this.selectedIndex = -1; this.selectedIndex = -1;
@ -252,58 +310,56 @@
} }
}, },
getQuery: function (val) { getQuery: function (value) {
var d, arr; var delimiter = this.options.delimiter,
d = this.options.delimiter; parts;
if (!d) {
return $.trim(val); if (!delimiter) {
return $.trim(value);
} }
arr = val.split(d); parts = value.split(delimiter);
return $.trim(arr[arr.length - 1]); return $.trim(parts[parts.length - 1]);
}, },
getSuggestionsLocal: function (q) { getSuggestionsLocal: function (q) {
var ret, arr, len, val, i;
arr = this.options.lookup;
len = arr.suggestions.length;
ret = { suggestions: [], data: [] };
q = q.toLowerCase(); q = q.toLowerCase();
for (i = 0; i < len; i++) { return {
val = arr.suggestions[i]; suggestions: $.grep(this.options.lookup, function (suggestion) {
if (val.toLowerCase().indexOf(q) === 0) { return suggestion.value.toLowerCase().indexOf(q) !== -1;
ret.suggestions.push(val); })
ret.data.push(arr.data[i]); };
}
}
return ret;
}, },
getSuggestions: function (q) { getSuggestions: function (q) {
var cr, me; var response,
that = this,
options = that.options;
cr = this.isLocal ? this.getSuggestionsLocal(q) : this.cachedResponse[q]; response = that.isLocal ? that.getSuggestionsLocal(q) : that.cachedResponse[q];
if (cr && $.isArray(cr.suggestions)) { if (response && $.isArray(response.suggestions)) {
this.suggestions = cr.suggestions; that.suggestions = response.suggestions;
this.data = cr.data; that.suggest();
this.suggest(); } else if (!that.isBadQuery(q)) {
} else if (!this.isBadQuery(q)) { that.options.params.query = q;
me = this; $.ajax({
me.options.params.query = q; url: options.serviceUrl,
$.get(this.serviceUrl, me.options.params, function (txt) { data: options.params,
me.processResponse(txt); type: options.type,
}, 'text'); dataType: 'text'
}).done(function (txt) {
that.processResponse(txt);
});
} }
}, },
isBadQuery: function (q) { isBadQuery: function (q) {
var i = this.badQueries.length; var badQueries = this.badQueries,
i = badQueries.length;
while (i--) { while (i--) {
if (q.indexOf(this.badQueries[i]) === 0) { if (q.indexOf(badQueries[i]) === 0) {
return true; return true;
} }
} }
@ -312,9 +368,9 @@
}, },
hide: function () { hide: function () {
this.enabled = false; this.visible = false;
this.selectedIndex = -1; this.selectedIndex = -1;
this.container.hide(); $(this.suggestionsContainer).hide();
}, },
suggest: function () { suggest: function () {
@ -323,53 +379,41 @@
return; return;
} }
var me, len, div, f, v, i, s, mOver, mClick; var len = this.suggestions.length,
formatResults = this.options.formatResult,
me = this; value = this.getQuery(this.currentValue),
len = this.suggestions.length; suggestion,
f = this.options.fnFormatResult; className = this.classes.suggestion,
v = this.getQuery(this.currentValue); classSelected = this.classes.selected,
container = $(this.suggestionsContainer),
mOver = function (xi) { html = '',
return function () { i;
me.activate(xi);
};
};
mClick = function (xi) {
return function () {
me.select(xi);
};
};
this.container.hide().empty();
// Build suggestions inner HTML:
for (i = 0; i < len; i++) { for (i = 0; i < len; i++) {
s = this.suggestions[i]; suggestion = this.suggestions[i];
div = $((me.selectedIndex === i ? '<div class="selected"' : '<div') + ' title="' + s + '">' + f(s, this.data[i], v) + '</div>'); html += '<div class="' + className + '" data-index="' + i + '">' + formatResults(suggestion, value) + '</div>';
div.mouseover(mOver(i));
div.click(mClick(i));
this.container.append(div);
} }
this.enabled = true; container.html(html).show();
this.container.show(); this.visible = true;
// Select first value by default:
this.selectedIndex = 0;
container.children().first().addClass(classSelected);
}, },
processResponse: function (text) { processResponse: function (text) {
/*jslint evil: true */ var response = $.parseJSON(text);
var response;
try { // If suggestions is string array, convert them to supported format:
response = eval('(' + text + ')'); if (typeof response.suggestions[0] === 'string') {
} catch (err) { response.suggestions = $.map(response.suggestions, function (value) {
return; return { value: value, data: null };
} });
if (!$.isArray(response.data)) {
response.data = [];
} }
// Cache results if cache is not disabled:
if (!this.options.noCache) { if (!this.options.noCache) {
this.cachedResponse[response.query] = response; this.cachedResponse[response.query] = response;
if (response.suggestions.length === 0) { if (response.suggestions.length === 0) {
@ -377,53 +421,37 @@
} }
} }
// Display suggestions only if returned query matches current value:
if (response.query === this.getQuery(this.currentValue)) { if (response.query === this.getQuery(this.currentValue)) {
this.suggestions = response.suggestions; this.suggestions = response.suggestions;
this.data = response.data;
this.suggest(); this.suggest();
} }
}, },
activate: function (index) { activate: function (index) {
var divs, activeItem; var activeItem,
selected = this.classes.selected,
container = $(this.suggestionsContainer),
children = container.children();
divs = this.container.children(); container.children('.' + selected).removeClass(selected);
// Clear previous selection:
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
$(divs.get(this.selectedIndex)).removeClass();
}
this.selectedIndex = index; this.selectedIndex = index;
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) { if (this.selectedIndex !== -1 && children.length > this.selectedIndex) {
activeItem = divs.get(this.selectedIndex); activeItem = children.get(this.selectedIndex);
$(activeItem).addClass('selected'); $(activeItem).addClass(selected);
return activeItem; return activeItem;
} }
return null; return null;
}, },
deactivate: function (div, index) {
div.className = '';
if (this.selectedIndex === index) {
this.selectedIndex = -1;
}
},
select: function (i) { select: function (i) {
var selectedValue, f; var selectedValue = this.suggestions[i];
selectedValue = this.suggestions[i];
if (selectedValue) { if (selectedValue) {
this.el.val(selectedValue); this.el.val(selectedValue);
if (this.options.autoSubmit) {
f = this.el.parents('form');
if (f.length > 0) {
f.get(0).submit();
}
}
this.ignoreValueChange = true; this.ignoreValueChange = true;
this.hide(); this.hide();
this.onSelect(i); this.onSelect(i);
@ -431,23 +459,19 @@
}, },
change: function (i) { change: function (i) {
var selectedValue, onChange, me, s, d; var onChange,
me = this,
me = this; selectedValue = this.suggestions[i],
selectedValue = this.suggestions[i]; suggestion;
if (selectedValue) { if (selectedValue) {
s = me.suggestions[i]; suggestion = me.suggestions[i];
d = me.data[i]; me.el.val(me.getValue(suggestion.value));
me.el.val(me.getValue(s));
} else {
s = '';
d = -1;
}
onChange = me.options.onChange; onChange = me.options.onChange;
if ($.isFunction(onChange)) { if ($.isFunction(onChange)) {
onChange(s, d, me.el); onChange(suggestion, me.el);
}
} }
}, },
@ -457,7 +481,7 @@
} }
if (this.selectedIndex === 0) { if (this.selectedIndex === 0) {
this.container.children().get(0).className = ''; $(this.suggestionsContainer).children().first().removeClass(this.classes.selected);
this.selectedIndex = -1; this.selectedIndex = -1;
this.el.val(this.currentValue); this.el.val(this.currentValue);
return; return;
@ -474,54 +498,80 @@
this.adjustScroll(this.selectedIndex + 1); this.adjustScroll(this.selectedIndex + 1);
}, },
adjustScroll: function (i) { adjustScroll: function (index) {
var activeItem, offsetTop, upperBound, lowerBound; var activeItem = this.activate(index),
offsetTop,
upperBound,
lowerBound,
heightDelta = 25;
activeItem = this.activate(i); if (!activeItem) {
offsetTop = activeItem.offsetTop; return;
upperBound = this.container.scrollTop();
lowerBound = upperBound + this.options.maxHeight - 25;
if (offsetTop < upperBound) {
this.container.scrollTop(offsetTop);
} else if (offsetTop > lowerBound) {
this.container.scrollTop(offsetTop - this.options.maxHeight + 25);
} }
this.el.val(this.getValue(this.suggestions[i])); offsetTop = activeItem.offsetTop;
upperBound = $(this.suggestionsContainer).scrollTop();
lowerBound = upperBound + this.options.maxHeight - heightDelta;
if (offsetTop < upperBound) {
$(this.suggestionsContainer).scrollTop(offsetTop);
} else if (offsetTop > lowerBound) {
$(this.suggestionsContainer).scrollTop(offsetTop - this.options.maxHeight + heightDelta);
}
this.el.val(this.getValue(this.suggestions[index].value));
}, },
onSelect: function (i) { onSelect: function (index) {
var me = this, var that = this,
callback = me.options.onSelect, onSelectCallback = that.options.onSelect,
sugestion = me.suggestions[i], suggestion = that.suggestions[index];
data = me.data[i];
me.el.val(me.getValue(sugestion)); that.el.val(that.getValue(suggestion.value));
if ($.isFunction(callback)) { if ($.isFunction(onSelectCallback)) {
callback(sugestion, data, me.el); onSelectCallback.call(that.element, suggestion);
} }
}, },
getValue: function (value) { getValue: function (value) {
var me = this, var that = this,
separator = me.options.delimiter, delimiter = that.options.delimiter,
currentValue, currentValue,
array; parts;
if (!separator) { if (!delimiter) {
return value; return value;
} }
currentValue = me.currentValue; currentValue = that.currentValue;
array = currentValue.split(separator); parts = currentValue.split(delimiter);
if (array.length === 1) { if (parts.length === 1) {
return value; return value;
} }
return currentValue.substr(0, currentValue.length - array[array.length - 1].length) + value; return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value;
} }
}; };
// Create chainable jQuery plugin:
$.fn.autocomplete = function (options, args) {
return this.each(function () {
var dataKey = 'autocomplete',
inputElement = $(this),
instance;
if (typeof options === 'string') {
instance = inputElement.data(dataKey);
if (typeof instance[options] === 'function') {
instance[options](args);
}
} else {
instance = new Autocomplete(this, options);
inputElement.data(dataKey, instance);
}
});
};
}(jQuery)); }(jQuery));