diff --git a/auto-complete.css b/auto-complete.css index 4261b1d..76a9488 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: 100%; 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 fba735f..7526f89 100644 --- a/auto-complete.js +++ b/auto-complete.js @@ -1,11 +1,11 @@ /* - JavaScript autoComplete v1.1.0 + 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 38+, Edge 13+, Safari 8+, Firefox 26+, Opera 25+, all mobile browsers +// Chrome 41+, Edge 15+, Safari 9+, Firefox 35+, Opera 28+, all mobile browsers var autoComplete = (function(){ // "use strict"; @@ -13,10 +13,10 @@ var autoComplete = (function(){ if (!document.querySelector) return; // helpers - function live(elClass, event, cb, context){ - (context || document).addEventListener(event, function(e){ - for (var t = e.target || e.srcElement; t; t = t.parentElement) - t.classList.contains(elClass) && (cb.call(t, e), t = 1); + 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); } @@ -29,6 +29,8 @@ var autoComplete = (function(){ offsetTop: 1, cache: 1, menuClass: '', + clickToShow: 1, + closeOnTap: 1, renderItem: function(item, search){ // escape special characters var si = document.createElement('div'); @@ -42,193 +44,257 @@ var autoComplete = (function(){ } return si; }, - renderItems: function (data, search, that){ + renderItems: function (data, search, ipt){ var tp = document.createElement('template'); var df = tp.content; - 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; } + } } + }; - window.addEventListener('resize', that.updateSC); - document.body.appendChild(that.sc); + // window.addEventListener('resize', ipt.updateSC); + + // ipt.pivot.appendChild(ipt.sc); + ipt.parentNode.insertBefore(ipt.pivot, ipt); // avoid Safari's minor layout bug - that.sc.addEventListener('mouseleave', function(e){ - var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); - if (sel) setTimeout(function(){ sel.classList.remove('selected'); }, 20); + 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 = that.sc.querySelector('.autocomplete-suggestion.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'); - }, that.sc); + }, ipt.sc); - live('autocomplete-suggestion', 'mousedown', function(e){ + live('.autocomplete-suggestion', 'pointerdown', function(e){ e.stopPropagation(); - var v = this.getAttribute('data-val'); - that.value = v; - o.onSelect(e, v, this); - that.sc.style.display = 'none'; - }, 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); - }; - that.addEventListener('blur', that.blurHandler); + e.preventDefault(); + if (!e.button) { // only left click + var v = ipt.value = this.getAttribute('data-val'); + o.onSelect(e, v, this); + o.closeOnTap && ipt.hideSC(); + } + }, ipt.sc); - var suggest = function(data, val){ - val = val || that.value; // PR#28 - that.cache.set(val, data); - that.triggerSC(data, val, val.length >= o.minChars); + 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); + } }; + // PR#40 // Optional method to trigger results programatically - that.triggerSC = function(data, val, b){ - if (data.length && b!==false) { - o.renderItems(data, (val || ''), that); - that.updateSC(0); + ipt.triggerSC = function(data, val, b){ + if (data.length && b !== false) { + o.renderItems(data, val, ipt); + ipt.updateSC(); + o.onRender(data, val, ipt); } else { - that.sc.style.display = 'none'; + ipt.hideSC(); } }; - that.keydownHandler = function(e){ - var key = window.event ? e.keyCode : e.which; - // down (40), up (38) - if ((key == 40 || key == 38) && that.sc.innerHTML) { - var next, sel = that.sc.querySelector('.autocomplete-suggestion.selected'); - if (!sel) { - next = (key == 40) ? that.sc.querySelector('.autocomplete-suggestion') : that.sc.childNodes[that.sc.childNodes.length - 1]; // first : last - next.classList.add('selected'); - that.value = next.getAttribute('data-val'); - } else { - next = (key == 40) ? sel.nextSibling : sel.previousSibling; - sel.classList.remove('selected'); + var inputCounter = 0; + + ipt.iptHandlers = { + blur(){ + ipt.last_val = ipt.value; + ipt.hideSC(); + }, + keydown(e){ + var key = window.event ? e.keyCode : e.which; + // down (40), up (38) + if ((key == 40 || key == 38) && ipt.sc.lastChild) { + var next, sel = ipt.sc.querySelector('.autocomplete-suggestion.selected'); + if (!sel) { + next = (key == 40) ? ipt.sc.querySelector('.autocomplete-suggestion') : ipt.sc.lastChild; // first : last + } else { + next = (key == 40) ? sel.nextSibling : sel.previousSibling; + sel.classList.remove('selected'); + } if (next) { next.classList.add('selected'); - that.value = next.getAttribute('data-val'); + ipt.value = next.getAttribute('data-val'); } else { - that.value = that.last_val; + ipt.value = ipt.last_val; next = 0; } - } - that.updateSC(0, next); - return false; - } - // esc - else if (key == 27) { that.value = that.last_val; that.sc.style.display = 'none'; } - // enter - else if (key == 13 || key == 9) { - var tsc = that.sc; - if (tsc.style.display !== 'none') { // PR#8 + ipt.updateSC(next); e.preventDefault(); } - var sel = tsc.querySelector('.autocomplete-suggestion.selected'); - if (sel && tsc.style.display != 'none') { - o.onSelect(e, sel.getAttribute('data-val'), sel); - setTimeout(function(){ tsc.style.display = 'none'; }, 20); - e.preventDefault(); + // 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; } - } - }; - that.addEventListener('keydown', that.keydownHandler); - - that.keyupHandler = function(e){ - var key = window.event ? e.keyCode : e.which; - if (!key || (key < 35 || key > 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); + }, + 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) { - var c = that.cache; - if (c.has(val)) { suggest(c.get(val)); return; } + var c = ipt.cache; + if (c.has(val)) { suggest(c.get(val), val); return; } // no requests if previous suggestions were empty var k = o.minChars; - for (var j=val.length-1; j>=k; j--) { + for (var j = val.length; --j >= k;) { var part = val.slice(0, j); - if (c.has(part) && !c.get(part).length) { suggest([]); return; } + if (c.has(part) && !c.get(part).length) { suggest([], val); return; } } } // PR#5 - that.timer = setTimeout(function(){ - var thisRequestId = ++that._currentRequestId; - o.source(val, function (data, val){ - if (thisRequestId === that._currentRequestId) return suggest(data, val); + 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(); } } - }; - that.addEventListener('keyup', that.keyupHandler); - that.focusHandler = function(e){ - that.last_val = '\n'; - that.keyupHandler(e) - }; - if (!o.minChars) that.addEventListener('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; + } + } - for (var i=0; i

autoComplete

An extremely lightweight and powerful vanilla JavaScript completion suggester.

- Download + Download   - View on GitHub + View on GitHub
- +
+ +

Overview and Features

@@ -111,6 +113,8 @@

Settings

offsetLeft0Optional left offset of the suggestions container. offsetTop1Optional top offset of the suggestions container. cache1Determines if performed searches should be cached. + clickToShow1Determines if clicking input should show dropdown. + closeOnTap1Determines if dropdown should close after tapping item. menuClass'' Custom class/es that get/s added to the dropdown menu container. @@ -136,6 +140,12 @@

Settings

term is the selected value. and item is the item rendered by the renderItem function. + onRender(data, val, ipt) + A callback function that fires after the suggestion list is rendered (and shown). + data is the data, + val is the value of input. + and ipt is the input element. +   Public Memebers @@ -167,6 +177,46 @@

Searching in local data

Simple as that.

+

Searching in local data, position="top" if available space below is not sufficient

+

+ Move dropdown upwards if available space below is not sufficent +

+
var updateDropDownPosition = function(ipt){
+    const pivot = ipt.pivot;
+    const sc = ipt.sc;
+    if(pivot && sc){
+        const rectSC = sc.getBoundingClientRect();
+        const rectIpt = ipt.getBoundingClientRect();
+        pivot.setAttribute('position', (rectSC.height+rectIpt.bottom>document.documentElement.clientHeight)?'top':'bottom');
+    }
+}
+var demoDynamic = new autoComplete({
+    selector: '#dynamic-demo',
+    minChars: 0,
+    source: function(term, response){
+        term = term.toLowerCase();
+        var choices = ['ActionScript', 'AppleScript', 'Asp', 'Assembly', 'BASIC', 'Batch', 'C', 'C++', 'CSS', 'Clojure', 'COBOL', 'ColdFusion', 'Erlang', 'Fortran', 'Groovy', 'Haskell', 'HTML', 'Java', 'JavaScript', 'Lisp', 'Perl', 'PHP', 'PowerShell', 'Python', 'Ruby', 'Scala', 'Scheme', 'SQL', 'TeX', 'XML'];
+        var suggestions = [];
+        for (var i=0;i=0) suggestions.push(choices[i]);
+        response(suggestions);
+    },
+    onRender: function(data, val, ipt){
+        updateDropDownPosition(ipt);
+    }
+});
+document.addEventListener('scroll', function(){
+    let ipt = document.querySelector('#dynamic-demo');
+    updateDropDownPosition(ipt);
+}, true);
+

+ Move dropdown upwards if available space below is not sufficent. +

+ +
+ +
+

AJAX requests

AJAX requests may come with very different requirements: JSON, JSONP, GET, POST, additionaly params, authentications, etc. @@ -269,7 +319,7 @@

Advanced suggestions handling and custom layout

Source code for this example:

new autoComplete({
-    selector: 'input[name=&quot;q&quot;]',
+    selector: 'input[name="q"]',
     minChars: 1,
     source: function(term, response){
         term = term.toLowerCase();
@@ -322,7 +372,29 @@ 

Advanced suggestions handling and custom layout

+
@@ -39,7 +39,7 @@ https://cdn.jsdelivr.net/gh/cyfung1031/JavaScript-autoComplete@1.1.0/auto-comple ## Demo and Documentation -https://raw.githack.com/cyfung1031/JavaScript-autoComplete/1.1.0/demo.html +https://raw.githack.com/culefa/JavaScript-autoComplete/1.2.0/demo.html ## Features @@ -50,6 +50,10 @@ https://raw.githack.com/cyfung1031/JavaScript-autoComplete/1.1.0/demo.html ## Changelog +### Version 1.2.0 - 2023/10/07 + +* Performance Fix with changes in CSS Layout, and Enhancement in Functionality + ### Version 1.1.0 - 2023/10/06 * Rolling out Update for 2015 - 2023 PRs