diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/auto-complete.css b/auto-complete.css index 4261b1d..5db9b08 100644 --- a/auto-complete.css +++ b/auto-complete.css @@ -1,9 +1,19 @@ .autocomplete-suggestions { text-align: left; cursor: default; border: 1px solid #ccc; border-top: 0; background: #fff; box-shadow: -1px 1px 3px rgba(0,0,0,.1); - - /* core styles should not be changed */ - position: absolute; display: none; z-index: 9999; max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box; + position: absolute; /* display: none; z-index: 9999; */ max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box; + left: auto; top: auto; width: 100%; margin: 0; contain: content; /* 1.2.0 */ } .autocomplete-suggestion { position: relative; padding: 0 .6em; line-height: 23px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 1.02em; color: #333; } .autocomplete-suggestion b { font-weight: normal; color: #1f8dd6; } .autocomplete-suggestion.selected { background: #f0f0f0; } + +autocomplete-holder { + overflow: visible; position: absolute; left: auto; top: auto; width: 100%; height: 0; z-index: 9999; box-sizing: border-box; margin:0; padding:0; border:0; contain: size layout; +} + +autocomplete-holder[position="top"] { + margin-top: 0 !important; +} +autocomplete-holder[position="top"] .autocomplete-suggestions { + transform: translateY(-100%); +} \ No newline at end of file diff --git a/auto-complete.js b/auto-complete.js index 2f5a88c..7526f89 100644 --- a/auto-complete.js +++ b/auto-complete.js @@ -1,31 +1,23 @@ /* - JavaScript autoComplete v1.0.4 + JavaScript autoComplete v1.2.0 Copyright (c) 2014 Simon Steinberger / Pixabay GitHub: https://github.com/Pixabay/JavaScript-autoComplete License: http://www.opensource.org/licenses/mit-license.php */ +// Chrome 41+, Edge 15+, Safari 9+, Firefox 35+, Opera 28+, all mobile browsers + var autoComplete = (function(){ // "use strict"; function autoComplete(options){ if (!document.querySelector) return; // helpers - function hasClass(el, className){ return el.classList ? el.classList.contains(className) : new RegExp('\\b'+ className+'\\b').test(el.className); } - - function addEvent(el, type, handler){ - if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler); - } - function removeEvent(el, type, handler){ - // if (el.removeEventListener) not working in IE11 - if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler); - } - function live(elClass, event, cb, context){ - addEvent(context || document, event, function(e){ - var found, el = e.target || e.srcElement; - while (el && !(found = hasClass(el, elClass))) el = el.parentElement; - if (found) cb.call(el, e); - }); + function live(selector, event, cb, context){ + context.addEventListener(event, function(e){ + var t = (e.target || e.srcElement).closest(selector); + t && cb.call(t, e); + }, true); } var o = { @@ -37,186 +29,292 @@ var autoComplete = (function(){ offsetTop: 1, cache: 1, menuClass: '', - renderItem: function (item, search){ + clickToShow: 1, + closeOnTap: 1, + renderItem: function(item, search){ // escape special characters - search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - var re = new RegExp("(" + search.split(' ').join('|') + ")", "gi"); - return '
' + item.replace(re, "$1") + '
'; + var si = document.createElement('div'); + si.className = 'autocomplete-suggestion'; + si.setAttribute('data-val', item); // see PR#86 + try{ + search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + si.innerHTML = item.replace(new RegExp("(" + search.split(' ').join('|') + ")", "gi"), "$1"); + }catch(e){ + si.textContent = item; + } + return si; + }, + renderItems: function (data, search, ipt){ + var tp = document.createElement('template'); + var df = tp.content; + for (var i = 0; i < data.length; i++) { + var item = data[i]; + var si = o.renderItem(item, search); + if (typeof si == 'string') tp.innerHTML += si; + else if (si && si.nodeType == 1) df.appendChild(si); + } + var _sc = ipt.sc; + var firstChild; + while (firstChild = _sc.firstChild) firstChild.remove(); + _sc.appendChild(df); }, - onSelect: function(e, term, item){} + onSelect: function(e, term, item){}, + onRender: function(data, val, ipt){}, }; for (var k in options) { if (options.hasOwnProperty(k)) o[k] = options[k]; } - // init - var elems = typeof o.selector == 'object' ? [o.selector] : document.querySelectorAll(o.selector); - for (var i=0; i 0) - that.sc.scrollTop = selTop + that.sc.suggestionHeight + scrTop - that.sc.maxHeight; - else if (selTop < 0) - that.sc.scrollTop = selTop + scrTop; + ipt.sc = document.createElement('div'); + ipt.sc.className = 'autocomplete-suggestions '+o.menuClass; + ipt.sc.style.display = 'none'; + + ipt.pivot = document.createElement('autocomplete-holder'); + ipt.autocompleteAttr = ipt.getAttribute('autocomplete'); + ipt.setAttribute('autocomplete', 'off'); + ipt.cache = new Map(); // changed from {} to Map; related to PR#37 PR#38 + ipt.last_val = ''; + + ipt.updateSize = function(){ + var pivot = ipt.pivot; + var rect = ipt.getBoundingClientRect(); + pivot.style.width = rect.width+'px'; + // pivot.style.height = rect.height+'px'; + pivot.style.marginTop = rect.height+'px'; + // pivot.style.marginBottom = -rect.height+'px'; + } + + ipt.isSCVisible = function(){ + var style = ipt.sc.style; + return style.display != 'none'; + } + ipt.hideSC = function(){ + var style = ipt.sc.style; + style.display != 'none' && (style.display = 'none'); + } + + ipt.showSC = function(){ + var style = ipt.sc.style; + style.display == 'none' && (style.display = 'block'); + } + + ipt.updateSC = function(next){ // see issue mentioned in PR#49 + + ipt.updateSize(); + var pivot = ipt.pivot; + var _sc = ipt.sc; + (_sc.parentNode == pivot) || pivot.appendChild(_sc); + (ipt.nextSibling == pivot) || ipt.parentNode.insertBefore(pivot, ipt); + ipt.showSC(); + + if (!next) _sc.scrollTop = 0; + else { + var rectSC = _sc.getBoundingClientRect(); + if (rectSC.height > 0) { + var rectNext = next.getBoundingClientRect(); + var a = rectNext.top - rectSC.top; + var b = rectNext.bottom - rectSC.bottom; + if (b > 0) { + _sc.scrollTop += b; + } else if (a < 0) { + _sc.scrollTop += a; } + } } - } - addEvent(window, 'resize', that.updateSC); - document.body.appendChild(that.sc); - - live('autocomplete-suggestion', 'mouseleave', function(e){ - var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); - if (sel) setTimeout(function(){ sel.className = sel.className.replace('selected', ''); }, 20); - }, that.sc); - - live('autocomplete-suggestion', 'mouseover', function(e){ - var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); - if (sel) sel.className = sel.className.replace('selected', ''); - this.className += ' selected'; - }, that.sc); - - live('autocomplete-suggestion', 'mousedown', function(e){ - if (hasClass(this, 'autocomplete-suggestion')) { // else outside click - var v = this.getAttribute('data-val'); - that.value = v; + + }; + // window.addEventListener('resize', ipt.updateSC); + + // ipt.pivot.appendChild(ipt.sc); + ipt.parentNode.insertBefore(ipt.pivot, ipt); // avoid Safari's minor layout bug + + ipt.sc.addEventListener('mouseleave', function(e){ + var sel = ipt.sc.querySelector('.autocomplete-suggestion.selected'); + sel.classList.remove('selected'); + }); + + live('.autocomplete-suggestion', 'mouseenter', function(e){ + var sel = ipt.sc.querySelector('.autocomplete-suggestion.selected'); + if (sel) sel.classList.remove('selected'); + this.classList.add('selected'); + }, ipt.sc); + + live('.autocomplete-suggestion', 'pointerdown', function(e){ + e.stopPropagation(); + e.preventDefault(); + if (!e.button) { // only left click + var v = ipt.value = this.getAttribute('data-val'); o.onSelect(e, v, this); - that.sc.style.display = 'none'; + o.closeOnTap && ipt.hideSC(); + } + }, ipt.sc); + + var suggest = function(data, val){ // see PR#28 + if (typeof val == 'string') { + (data instanceof Array) && ipt.cache.set(val, data); + ipt.triggerSC(data, val, val.length >= o.minChars); } - }, that.sc); - - that.blurHandler = function(){ - try { var over_sb = document.querySelector('.autocomplete-suggestions:hover'); } catch(e){ var over_sb = 0; } - if (!over_sb) { - that.last_val = that.value; - that.sc.style.display = 'none'; - setTimeout(function(){ that.sc.style.display = 'none'; }, 350); // hide suggestions on fast input - } else if (that !== document.activeElement) setTimeout(function(){ that.focus(); }, 20); }; - addEvent(that, 'blur', that.blurHandler); - - var suggest = function(data){ - var val = that.value; - that.cache[val] = data; - if (data.length && val.length >= o.minChars) { - var s = ''; - for (var i=0;i 40) && key != 13 && key != 27) { - var val = that.value; - if (val.length >= o.minChars) { - if (val != that.last_val) { - that.last_val = val; - clearTimeout(that.timer); + // esc + else if (key == 27) { ipt.value = ipt.last_val; ipt.hideSC(); } + // enter + else if (key == 13 || key == 9) { + var tsc = ipt.sc; + var isVisible = ipt.isSCVisible(); + var sel = tsc.querySelector('.autocomplete-suggestion.selected'); + if (sel && isVisible) { + o.onSelect(e, sel.getAttribute('data-val'), sel); + ipt.hideSC(); + } + if (isVisible) { // PR#8 + e.preventDefault(); + } + ipt.last_val = ipt.value; + } + }, + input(){ + inputCounter++; + }, + keyup(e){ + + if (!inputCounter) return; + inputCounter = 0; + var key = window.event ? e.keyCode : e.which; + if (!key || (key < 35 || key > 40) && key != 13 && key != 27) { + var val = ipt.value; + ipt.last_val = val; + if (val.length >= o.minChars) { + ipt.timer && clearTimeout(ipt.timer); + ipt.timer = 0; if (o.cache) { - if (val in that.cache) { suggest(that.cache[val]); return; } + var c = ipt.cache; + if (c.has(val)) { suggest(c.get(val), val); return; } // no requests if previous suggestions were empty - for (var i=1; i= k;) { + var part = val.slice(0, j); + if (c.has(part) && !c.get(part).length) { suggest([], val); return; } } } - that.timer = setTimeout(function(){ o.source(val, suggest) }, o.delay); + // PR#5 + ipt.timer = setTimeout(function(){ + var thisRequestId = ++ipt._currentRequestId; + var _val = ipt.value; + o.source(_val, function (data, val){ + if (thisRequestId == ipt._currentRequestId){ + suggest(data, typeof val === 'string' ? val : _val); + } + }); + }, o.delay); + } else { + ipt.sc.style.display = 'none'; } - } else { - that.last_val = val; - that.sc.style.display = 'none'; + } + }, + // focus(e){ + // if (!o.minChars) { + // ipt.last_val = '\n'; + // ipt.iptHandlers.keyup(e); + // } + // }, + + click(){ + if (o.clickToShow && ipt.isContentNotEmpty()) { + ipt.showSC(); } } - }; - addEvent(that, 'keyup', that.keyupHandler); - that.focusHandler = function(e){ - that.last_val = '\n'; - that.keyupHandler(e) - }; - if (!o.minChars) addEvent(that, 'focus', that.focusHandler); + } + + for (var h of Object.keys(ipt.iptHandlers)) { + ipt.addEventListener(h, ipt.iptHandlers[h]); + } + + ipt.destroyAutoComplete = function(){ + if (ipt) { + // window.removeEventListener('resize', ipt.updateSC); + for (var h of Object.keys(ipt.iptHandlers)) { + ipt.removeEventListener(h, ipt.iptHandlers[h]); + } + if (ipt.autocompleteAttr) ipt.setAttribute('autocomplete', ipt.autocompleteAttr); + else ipt.removeAttribute('autocomplete'); + ipt.pivot && ipt.pivot.remove(); // issue#92 PR#93 + ipt = null; + } + } + + ipt.isContentNotEmpty = function () { + return (ipt.value || '').length > 0 && (ipt.sc.textContent || '').length > 0; + } + + } + // init + var elems = typeof o.selector == 'object' ? [o.selector] : document.querySelectorAll(o.selector); + for (var i = 0; i < elems.length; i++) { + forEach(elems[i]); } // public destroy method this.destroy = function(){ - for (var i=0; i'+e.replace(o,"$1")+""},onSelect:function(){}};for(var c in e)e.hasOwnProperty(c)&&(l[c]=e[c]);for(var a="object"==typeof l.selector?[l.selector]:document.querySelectorAll(l.selector),u=0;u0?i.sc.scrollTop=n+i.sc.suggestionHeight+s-i.sc.maxHeight:0>n&&(i.sc.scrollTop=n+s)}else i.sc.scrollTop=0},o(window,"resize",i.updateSC),document.body.appendChild(i.sc),n("autocomplete-suggestion","mouseleave",function(){var e=i.sc.querySelector(".autocomplete-suggestion.selected");e&&setTimeout(function(){e.className=e.className.replace("selected","")},20)},i.sc),n("autocomplete-suggestion","mouseover",function(){var e=i.sc.querySelector(".autocomplete-suggestion.selected");e&&(e.className=e.className.replace("selected","")),this.className+=" selected"},i.sc),n("autocomplete-suggestion","mousedown",function(e){if(t(this,"autocomplete-suggestion")){var o=this.getAttribute("data-val");i.value=o,l.onSelect(e,o,this),i.sc.style.display="none"}},i.sc),i.blurHandler=function(){try{var e=document.querySelector(".autocomplete-suggestions:hover")}catch(t){var e=0}e?i!==document.activeElement&&setTimeout(function(){i.focus()},20):(i.last_val=i.value,i.sc.style.display="none",setTimeout(function(){i.sc.style.display="none"},350))},o(i,"blur",i.blurHandler);var r=function(e){var t=i.value;if(i.cache[t]=e,e.length&&t.length>=l.minChars){for(var o="",s=0;st||t>40)&&13!=t&&27!=t){var o=i.value;if(o.length>=l.minChars){if(o!=i.last_val){if(i.last_val=o,clearTimeout(i.timer),l.cache){if(o in i.cache)return void r(i.cache[o]);for(var s=1;s - +