Created
January 13, 2011 20:58
-
-
Save katspaugh/778591 to your computer and use it in GitHub Desktop.
Displays a suggestions box.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function (globals, exports, $, doc) { | |
/* | |
** Takes one @options argument with properties: | |
** - 'list' -- a hashtable or array of possible suggestions, | |
** - 'propName' -- a property with suggestions in the hashtable, | |
** - 'input' -- an input element or input selector, | |
** - 'callback' -- what to do on option select, | |
** - 'focusFirst' -- whether to set focus on the first option, | |
** - 'meaningfulLength' -- how many characters can provide meaning. | |
*/ | |
function SuggestInput() { | |
this.constructor = SuggestInput; | |
return this.init.apply(this, arguments); | |
} | |
SuggestInput.prototype = { | |
Timeouts: { | |
blur: 300, | |
resize: 10 | |
}, | |
ClassNames: { | |
select : 'suggest-select', | |
focus : 'suggest-focus' | |
}, | |
init: function (options) { | |
var inputElement = $(options.input); | |
if (!inputElement.length) { | |
return null; | |
} | |
this.settings = { | |
meaningfulLength: options.meaningfulLength || 3, | |
focusFirst: options.focusFirst || false | |
}; | |
this.elements = { | |
container: $(doc.body), | |
input: inputElement | |
}; | |
this.elements.select = this.createSelect(); | |
this.list = options.list; | |
this.selectCallback = options.callback; | |
if (null != options.propName) { | |
this.listIsHash = true; | |
this.propName = options.propName; | |
} | |
this.value = this.elements.input.val() || ''; | |
this.bindInput(); | |
this.bindSelect(); | |
this.bindDocument(); | |
return this; | |
}, | |
bindDocument: function () { | |
var suggest = this; | |
$(doc).bind('keydown', function (e) { | |
if (suggest.visible) { | |
var code = e.keyCode; | |
switch (code) { | |
/* arrow up */ | |
case 38: { | |
e.preventDefault(); | |
suggest.moveFocus(-1); | |
break; | |
} | |
/* arrow down */ | |
case 40: { | |
e.preventDefault(); | |
suggest.moveFocus(1); | |
break; | |
} | |
/* esc */ | |
case 27: { | |
e.preventDefault(); | |
suggest.hideSelect(); | |
break; | |
} | |
}; | |
} | |
}); | |
var resizeTimeout; | |
$(window).bind('resize', function () { | |
if (suggest.visible) { | |
resizeTimeout && clearTimeout(resizeTimeout); | |
resizeTimeout = setTimeout( | |
function () { | |
suggest.setSelectPosition(); | |
}, | |
suggest.Timeouts.resize | |
); | |
} | |
}); | |
}, | |
bindInput: function () { | |
var suggest = this, | |
blurDelay; | |
suggest.elements.input.attr('autocomplete', 'off'); | |
var ENTER = 13; | |
suggest.elements.input.bind({ | |
'keydown': function (e) { | |
if (ENTER == e.keyCode && suggest.visible) { | |
e.preventDefault(); | |
} | |
}, | |
'keyup': function (e) { | |
if (ENTER == e.keyCode && suggest.visible) { | |
var val = ''; | |
if (suggest.visible) { | |
val = suggest.getFocusedValue(); | |
} else { | |
val = $(e.target).val(); | |
} | |
suggest.onSelect(val); | |
} else { | |
suggest.setValue(); | |
} | |
}, | |
'change': function () { | |
suggest.setValue(); | |
}, | |
'blur': function () { | |
if (suggest.visible) { | |
blurDelay && clearTimeout(blurDelay); | |
blurDelay = setTimeout(function () { | |
suggest.hideSelect(); | |
}, suggest.Timeouts.blur); | |
} | |
} | |
}); | |
}, | |
setSelectPosition: function () { | |
var input = this.elements.input, | |
offset = input.offset(), | |
margin = input.outerHeight(); | |
return this.elements.select.css({ | |
top: offset.top + margin, | |
left: offset.left | |
}); | |
}, | |
showSelect: function () { | |
this.setSelectPosition().show().scrollTop(0); | |
if (this.settings.focusFirst) { | |
this.focusFirst(); | |
} | |
this.visible = true; | |
}, | |
focusFirst: function () { | |
this.elements.focusedOption = $('div:first', this.elements.select); | |
this.toggleHighlight(true); | |
}, | |
hideSelect: function () { | |
if (this.visible) { | |
this.elements.select.hide(); | |
this.visible = false; | |
} | |
}, | |
getFocusedValue: function () { | |
var retVal = this.value; | |
if (this.elements.focusedOption) { | |
var value = this.elements.focusedOption.data('value'); | |
if (null != value) { | |
retVal = value; | |
} | |
} | |
return retVal; | |
}, | |
setValue: function () { | |
var val = this.elements.input.val(); | |
if (val) { | |
if (val.toLowerCase() !== this.value.toLowerCase()) { | |
this.value = val; | |
this.suggest(); | |
} | |
} else { | |
this.hideSelect(); | |
} | |
}, | |
bindSelect: function () { | |
var suggest = this; | |
suggest.elements.select.bind({ | |
'click': function (e) { | |
e.preventDefault(); | |
var el = $(e.target), | |
val = el.data('value'); | |
if (null != val) { | |
suggest.elements.focusedOption = el; | |
suggest.onSelect(val); | |
} | |
}, | |
'mouseover': function (e) { | |
if (e.target !== this) { | |
if (suggest.elements.focusedOption) { | |
suggest.toggleHighlight(false); | |
} | |
suggest.elements.focusedOption = $(e.target); | |
suggest.toggleHighlight(true); | |
} | |
} | |
}); | |
}, | |
setInput: function (val) { | |
if (null != val) { | |
this.value = val; | |
this.elements.input.val(val); | |
} | |
}, | |
moveFocus: function (d) { | |
if (this.elements.focusedOption) { | |
this.toggleHighlight(false); | |
var next = this.elements.focusedOption[d > 0 ? 'next': 'prev'](); | |
this.elements.focusedOption = (next.length ? next : null); | |
} | |
if (!this.elements.focusedOption) { | |
this.elements.focusedOption = $( | |
'div:' + (d > 0 ? 'first' : 'last'), | |
this.elements.select | |
); | |
} | |
this.toggleHighlight(true); | |
this.adjustScroll(); | |
}, | |
toggleHighlight: function (toggle) { | |
this.elements.focusedOption.toggleClass( | |
this.ClassNames.focus, | |
toggle | |
); | |
}, | |
adjustScroll: function () { | |
var el = this.elements.focusedOption, | |
select = this.elements.select, | |
elHeight = el.outerHeight(), | |
offset = el.position()['top'] + elHeight, | |
height = select.outerHeight(), | |
sTop = select.scrollTop(); | |
if (offset < 0) { | |
select.scrollTop(0); | |
} | |
else if (offset < elHeight) { | |
select.scrollTop(sTop - height + elHeight); | |
} | |
else if (offset > height) { | |
select.scrollTop(sTop + offset - elHeight); | |
} | |
}, | |
onSelect: function (val) { | |
this.hideSelect(); | |
this.setInput(val); | |
if (this.selectCallback) { | |
this.selectCallback(val); | |
} | |
}, | |
suggest: function () { | |
var list = this.getFilteredList(); | |
if (list.length) { | |
var options = this.createOptionElements(list); | |
this.elements.select | |
.detach() | |
.empty() | |
.append(options) | |
.appendTo(this.elements.container); | |
this.showSelect(); | |
} else { | |
this.hideSelect(); | |
} | |
}, | |
getFilteredList: function () { | |
var filtered = [], | |
item, i; | |
if (!this.value) { | |
return filtered; | |
} | |
if (this.listIsHash) { | |
for (i in this.list) { | |
if (this.list.hasOwnProperty(i) && null != this.list[i]) { | |
item = this.list[i][this.propName]; | |
if (this.checkItem(item)) { | |
filtered.push(item); | |
} | |
} | |
} | |
} else { | |
for (i = 0; i < this.list.length; i++) { | |
item = this.list[i]; | |
if (this.checkItem(item)) { | |
filtered.push(item); | |
} | |
} | |
} | |
return filtered.sort(); | |
}, | |
checkItem: function (item) { | |
var match = false; | |
if (item) { | |
var value = this.value.toLowerCase(), | |
words = item.toLowerCase(); | |
match = (0 === words.indexOf(value)); | |
if (!match && value.length >= this.settings.meaningfulLength) { | |
match = new RegExp('\\s' + value, 'g').test(words); | |
} | |
} | |
return match; | |
}, | |
createSelect: function () { | |
var select = $('<div />'); | |
select.css({ | |
display : 'none', | |
position : 'absolute', | |
zIndex : 1000, | |
minWidth : this.elements.input.outerWidth() | |
}).attr( | |
'class', this.ClassNames.select | |
).appendTo(this.elements.container); | |
return select; | |
}, | |
createOptionElements: function (list) { | |
var options = [], item, i; | |
for (i = 0; i < list.length; i++) { | |
item = list[i]; | |
options[i] = $('<div />') | |
.data('value', item) | |
.text(item) | |
.get(0); | |
} | |
return $(options); | |
} | |
}; | |
if (!exports.SuggestInput) { | |
exports['SuggestInput'] = SuggestInput; | |
} | |
}(this, this, jQuery, document)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
TODO: use reducing of subsets for consecutive completions