Skip to content

Instantly share code, notes, and snippets.

@ccorcos
Created April 21, 2014 04:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ccorcos/11132176 to your computer and use it in GitHub Desktop.
Save ccorcos/11132176 to your computer and use it in GitHub Desktop.
complete.ly (js) customized for semantic-ui
/**
* 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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
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