/** * complete.ly 1.0.0 * MIT Licensing * Copyright (c) 2013 Lorenzo Puccetti * * This Software shall be used for doing good things, not bad things. * **/ 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.outline = '0'; dropDown.style.margin = '0'; dropDown.style.padding = '0'; 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[i].substring(token.length)+''; 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, '>'); 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 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; }