Created
April 21, 2014 04:24
-
-
Save ccorcos/11132176 to your computer and use it in GitHub Desktop.
complete.ly (js) customized for semantic-ui
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
/** | |
* complete.ly 1.0.0 | |
* MIT Licensing | |
* Copyright (c) 2013 Lorenzo Puccetti | |
* | |
* This Software shall be used for doing good things, not bad things. | |
* | |
**/ | |
completely = function completely(container, config) { | |
config = config || {}; | |
config.fontSize = config.fontSize || '16px'; | |
config.fontFamily = config.fontFamily || 'sans-serif'; | |
config.promptInnerHTML = config.promptInnerHTML || ''; | |
config.color = config.color || '#333'; | |
config.hintColor = config.hintColor || '#aaa'; | |
config.backgroundColor = config.backgroundColor || '#fff'; | |
config.dropDownBorderColor = config.dropDownBorderColor || '#aaa'; | |
config.dropDownZIndex = config.dropDownZIndex || '100'; // to ensure we are in front of everybody | |
config.dropDownOnHoverBackgroundColor = config.dropDownOnHoverBackgroundColor || '#ddd'; | |
var txtInput = document.createElement('input'); | |
txtInput.type = 'text'; | |
txtInput.spellcheck = false; | |
txtInput.style.fontSize = config.fontSize; | |
txtInput.style.fontFamily = config.fontFamily; | |
// txtInput.style.color = config.color; | |
// txtInput.style.backgroundColor = config.backgroundColor; | |
txtInput.style.width = '100%'; | |
// txtInput.style.outline = '0'; | |
// txtInput.style.border = '0'; | |
// txtInput.style.margin = '0'; | |
// txtInput.style.padding = '0'; | |
var txtHint = txtInput.cloneNode(); | |
txtHint.disabled = ''; | |
txtHint.style.position = 'absolute'; | |
txtHint.style.top = '0'; | |
txtHint.style.left = '0'; | |
txtHint.style.borderColor = 'transparent'; | |
txtHint.style.boxShadow = 'none'; | |
txtHint.style.color = config.hintColor; | |
txtInput.style.backgroundColor = 'transparent'; | |
txtInput.style.verticalAlign = 'top'; | |
txtInput.style.position = 'relative'; | |
var wrapper = document.createElement('div'); | |
wrapper.style.position = 'relative'; | |
wrapper.style.outline = '0'; | |
wrapper.style.border = '0'; | |
wrapper.style.margin = '0'; | |
wrapper.style.padding = '0'; | |
var prompt = document.createElement('div'); | |
prompt.style.position = 'absolute'; | |
prompt.style.outline = '0'; | |
prompt.style.margin = '0'; | |
prompt.style.padding = '0'; | |
prompt.style.border = '0'; | |
prompt.style.fontSize = config.fontSize; | |
prompt.style.fontFamily = config.fontFamily; | |
prompt.style.color = config.color; | |
prompt.style.backgroundColor = config.backgroundColor; | |
prompt.style.top = '0'; | |
prompt.style.left = '0'; | |
prompt.style.overflow = 'hidden'; | |
prompt.innerHTML = config.promptInnerHTML; | |
prompt.style.background = 'transparent'; | |
if (document.body === undefined) { | |
throw 'document.body is undefined. The library was wired up incorrectly.'; | |
} | |
document.body.appendChild(prompt); | |
var w = prompt.getBoundingClientRect().right; // works out the width of the prompt. | |
wrapper.appendChild(prompt); | |
prompt.style.visibility = 'visible'; | |
prompt.style.left = '-' + w + 'px'; | |
wrapper.style.marginLeft = w + 'px'; | |
wrapper.appendChild(txtHint); | |
wrapper.appendChild(txtInput); | |
var dropDown = document.createElement('div'); | |
dropDown.style.position = 'absolute'; | |
dropDown.style.visibility = 'hidden'; | |
dropDown.style.width = '100%'; | |
dropDown.style.outline = '0'; | |
dropDown.style.margin = '0'; | |
dropDown.style.padding = '0.2em'; | |
dropDown.style.textAlign = 'left'; | |
dropDown.style.fontSize = config.fontSize; | |
dropDown.style.fontFamily = config.fontFamily; | |
dropDown.style.backgroundColor = config.backgroundColor; | |
dropDown.style.zIndex = config.dropDownZIndex; | |
dropDown.style.cursor = 'default'; | |
dropDown.style.borderStyle = 'solid'; | |
dropDown.style.borderWidth = '1px'; | |
dropDown.style.borderColor = config.dropDownBorderColor; | |
dropDown.style.overflowX = 'hidden'; | |
dropDown.style.whiteSpace = 'pre'; | |
dropDown.style.overflowY = 'scroll'; // note: this might be ugly when the scrollbar is not required. however in this way the width of the dropDown takes into account | |
var createDropDownController = function(elem) { | |
var rows = []; | |
var ix = 0; | |
var oldIndex = -1; | |
var onMouseOver = function() { | |
this.style.outline = '1px solid #ddd'; | |
} | |
var onMouseOut = function() { | |
this.style.outline = '0'; | |
} | |
var onMouseDown = function() { | |
p.hide(); | |
p.onmouseselection(this.__hint); | |
} | |
var p = { | |
hide: function() { | |
elem.style.visibility = 'hidden'; | |
}, | |
refresh: function(token, array) { | |
elem.style.visibility = 'hidden'; | |
ix = 0; | |
elem.innerHTML = ''; | |
var vph = (window.innerHeight || document.documentElement.clientHeight); | |
var rect = elem.parentNode.getBoundingClientRect(); | |
var distanceToTop = rect.top - 6; // heuristic give 6px | |
var distanceToBottom = vph - rect.bottom - 6; // distance from the browser border. | |
rows = []; | |
for (var i = 0; i < array.length; i++) { | |
if (array[i].indexOf(token) !== 0) { | |
continue; | |
} | |
var divRow = document.createElement('div'); | |
divRow.style.color = config.color; | |
divRow.onmouseover = onMouseOver; | |
divRow.onmouseout = onMouseOut; | |
divRow.onmousedown = onMouseDown; | |
divRow.__hint = array[i]; | |
divRow.innerHTML = token + '<b>' + array[i].substring(token.length) + '</b>'; | |
rows.push(divRow); | |
elem.appendChild(divRow); | |
} | |
if (rows.length === 0) { | |
return; // nothing to show. | |
} | |
if (rows.length === 1 && token === rows[0].__hint) { | |
return; // do not show the dropDown if it has only one element which matches what we have just displayed. | |
} | |
if (rows.length < 2) return; | |
p.highlight(0); | |
if (distanceToTop > distanceToBottom * 3) { // Heuristic (only when the distance to the to top is 4 times more than distance to the bottom | |
elem.style.maxHeight = distanceToTop + 'px'; // we display the dropDown on the top of the input text | |
elem.style.top = ''; | |
elem.style.bottom = '100%'; | |
} else { | |
elem.style.top = '100%'; | |
elem.style.bottom = ''; | |
elem.style.maxHeight = distanceToBottom + 'px'; | |
} | |
elem.style.visibility = 'visible'; | |
}, | |
highlight: function(index) { | |
if (oldIndex != -1 && rows[oldIndex]) { | |
rows[oldIndex].style.backgroundColor = config.backgroundColor; | |
} | |
rows[index].style.backgroundColor = config.dropDownOnHoverBackgroundColor; // <-- should be config | |
oldIndex = index; | |
}, | |
move: function(step) { // moves the selection either up or down (unless it's not possible) step is either +1 or -1. | |
if (elem.style.visibility === 'hidden') return ''; // nothing to move if there is no dropDown. (this happens if the user hits escape and then down or up) | |
if (ix + step === -1 || ix + step === rows.length) return rows[ix].__hint; // NO CIRCULAR SCROLLING. | |
ix += step; | |
p.highlight(ix); | |
return rows[ix].__hint; //txtShadow.value = uRows[uIndex].__hint ; | |
}, | |
onmouseselection: function() {} // it will be overwritten. | |
}; | |
return p; | |
} | |
var dropDownController = createDropDownController(dropDown); | |
dropDownController.onmouseselection = function(text) { | |
txtInput.value = txtHint.value = leftSide + text; | |
rs.onChange(txtInput.value); // <-- forcing it. | |
registerOnTextChangeOldValue = txtInput.value; // <-- ensure that mouse down will not show the dropDown now. | |
setTimeout(function() { | |
txtInput.focus(); | |
}, 0); // <-- I need to do this for IE | |
} | |
wrapper.appendChild(dropDown); | |
container.appendChild(wrapper); | |
var spacer; | |
var leftSide; // <-- it will contain the leftSide part of the textfield (the bit that was already autocompleted) | |
function calculateWidthForText(text) { | |
if (spacer === undefined) { // on first call only. | |
spacer = document.createElement('span'); | |
spacer.style.visibility = 'hidden'; | |
spacer.style.position = 'fixed'; | |
spacer.style.outline = '0'; | |
spacer.style.margin = '0'; | |
spacer.style.padding = '0'; | |
spacer.style.border = '0'; | |
spacer.style.left = '0'; | |
spacer.style.whiteSpace = 'pre'; | |
spacer.style.fontSize = config.fontSize; | |
spacer.style.fontFamily = config.fontFamily; | |
spacer.style.fontWeight = 'normal'; | |
document.body.appendChild(spacer); | |
} | |
// Used to encode an HTML string into a plain text. | |
// taken from http://stackoverflow.com/questions/1219860/javascript-jquery-html-encoding | |
spacer.innerHTML = String(text).replace(/&/g, '&') | |
.replace(/"/g, '"') | |
.replace(/'/g, ''') | |
.replace(/</g, '<') | |
.replace(/>/g, '>'); | |
return spacer.getBoundingClientRect().right; | |
} | |
var rs = { | |
onArrowDown: function() {}, // defaults to no action. | |
onArrowUp: function() {}, // defaults to no action. | |
onEnter: function() {}, // defaults to no action. | |
onTab: function() {}, // defaults to no action. | |
onChange: function() { | |
rs.repaint() | |
}, // defaults to repainting. | |
startFrom: 0, | |
options: [], | |
wrapper: wrapper, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations) | |
input: txtInput, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations) | |
hint: txtHint, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations) | |
dropDown: dropDown, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations) | |
prompt: prompt, | |
setText: function(text) { | |
txtHint.value = text; | |
txtInput.value = text; | |
}, | |
getText: function() { | |
return txtInput.value; | |
}, | |
hideDropDown: function() { | |
dropDownController.hide(); | |
}, | |
repaint: function() { | |
var text = txtInput.value; | |
var startFrom = rs.startFrom; | |
var options = rs.options; | |
var optionsLength = options.length; | |
// breaking text in leftSide and token. | |
var token = text.substring(startFrom); | |
leftSide = text.substring(0, startFrom); | |
// updating the hint. | |
txtHint.value = ''; | |
for (var i = 0; i < optionsLength; i++) { | |
var opt = options[i]; | |
if (opt.indexOf(token) === 0) { // <-- how about upperCase vs. lowercase | |
txtHint.value = leftSide + opt; | |
break; | |
} | |
} | |
// moving the dropDown and refreshing it. | |
dropDown.style.left = calculateWidthForText(leftSide) + 'px'; | |
dropDownController.refresh(token, rs.options); | |
} | |
}; | |
var registerOnTextChangeOldValue; | |
/** | |
* Register a callback function to detect changes to the content of the input-type-text. | |
* Those changes are typically followed by user's action: a key-stroke event but sometimes it might be a mouse click. | |
**/ | |
var registerOnTextChange = function(txt, callback) { | |
registerOnTextChangeOldValue = txt.value; | |
var handler = function() { | |
var value = txt.value; | |
if (registerOnTextChangeOldValue !== value) { | |
registerOnTextChangeOldValue = value; | |
callback(value); | |
} | |
}; | |
// | |
// For user's actions, we listen to both input events and key up events | |
// It appears that input events are not enough so we defensively listen to key up events too. | |
// source: http://help.dottoro.com/ljhxklln.php | |
// | |
// The cost of listening to three sources should be negligible as the handler will invoke callback function | |
// only if the text.value was effectively changed. | |
// | |
// | |
if (txt.addEventListener) { | |
txt.addEventListener("input", handler, false); | |
txt.addEventListener('keyup', handler, false); | |
txt.addEventListener('change', handler, false); | |
} else { // is this a fair assumption: that attachEvent will exist ? | |
txt.attachEvent('oninput', handler); // IE<9 | |
txt.attachEvent('onkeyup', handler); // IE<9 | |
txt.attachEvent('onchange', handler); // IE<9 | |
} | |
}; | |
registerOnTextChange(txtInput, function(text) { // note the function needs to be wrapped as API-users will define their onChange | |
rs.onChange(text); | |
}); | |
var keyDownHandler = function(e) { | |
e = e || window.event; | |
var keyCode = e.keyCode; | |
if (keyCode == 33) { | |
return; | |
} // page up (do nothing) | |
if (keyCode == 34) { | |
return; | |
} // page down (do nothing); | |
if (keyCode == 27) { //escape | |
dropDownController.hide(); | |
txtHint.value = txtInput.value; // ensure that no hint is left. | |
txtInput.focus(); | |
return; | |
} | |
if (keyCode == 39 || keyCode == 35 || keyCode == 9) { // right, end, tab (autocomplete triggered) | |
if (keyCode == 9) { // for tabs we need to ensure that we override the default behaviour: move to the next focusable HTML-element | |
e.preventDefault(); | |
e.stopPropagation(); | |
if (txtHint.value.length == 0) { | |
rs.onTab(); // tab was called with no action. | |
// users might want to re-enable its default behaviour or handle the call somehow. | |
} | |
} | |
if (txtHint.value.length > 0) { // if there is a hint | |
dropDownController.hide(); | |
txtInput.value = txtHint.value; | |
var hasTextChanged = registerOnTextChangeOldValue != txtInput.value | |
registerOnTextChangeOldValue = txtInput.value; // <-- to avoid dropDown to appear again. | |
// for example imagine the array contains the following words: bee, beef, beetroot | |
// user has hit enter to get 'bee' it would be prompted with the dropDown again (as beef and beetroot also match) | |
if (hasTextChanged) { | |
rs.onChange(txtInput.value); // <-- forcing it. | |
} | |
} | |
return; | |
} | |
if (keyCode == 13) { // enter (autocomplete triggered) | |
if (txtHint.value.length == 0) { // if there is a hint | |
rs.onEnter(); | |
} else { | |
var wasDropDownHidden = (dropDown.style.visibility == 'hidden'); | |
dropDownController.hide(); | |
if (wasDropDownHidden) { | |
txtHint.value = txtInput.value; // ensure that no hint is left. | |
txtInput.focus(); | |
rs.onEnter(); | |
return; | |
} | |
txtInput.value = txtHint.value; | |
var hasTextChanged = registerOnTextChangeOldValue != txtInput.value | |
registerOnTextChangeOldValue = txtInput.value; // <-- to avoid dropDown to appear again. | |
// for example imagine the array contains the following words: bee, beef, beetroot | |
// user has hit enter to get 'bee' it would be prompted with the dropDown again (as beef and beetroot also match) | |
if (hasTextChanged) { | |
rs.onChange(txtInput.value); // <-- forcing it. | |
} | |
} | |
return; | |
} | |
if (keyCode == 40) { // down | |
var m = dropDownController.move(+1); | |
if (m == '') { | |
rs.onArrowDown(); | |
} | |
txtHint.value = leftSide + m; | |
return; | |
} | |
if (keyCode == 38) { // up | |
var m = dropDownController.move(-1); | |
if (m == '') { | |
rs.onArrowUp(); | |
} | |
txtHint.value = leftSide + m; | |
e.preventDefault(); | |
e.stopPropagation(); | |
return; | |
} | |
// it's important to reset the txtHint on key down. | |
// think: user presses a letter (e.g. 'x') and never releases... you get (xxxxxxxxxxxxxxxxx) | |
// and you would see still the hint | |
txtHint.value = ''; // resets the txtHint. (it might be updated onKeyUp) | |
}; | |
if (txtInput.addEventListener) { | |
txtInput.addEventListener("keydown", keyDownHandler, false); | |
} else { // is this a fair assumption: that attachEvent will exist ? | |
txtInput.attachEvent('onkeydown', keyDownHandler); // IE<9 | |
} | |
return rs; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment