github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/docs/themes/hugo-theme-relearn/static/js/auto-complete.js (about) 1 /* 2 JavaScript autoComplete v1.0.4+ 3 McShelby/hugo-theme-relearn#155 4 - PR #46, PR #75: introducing selectorToInsert and anchor to it 5 - sticky dropdown on scrolling 6 McShelby/hugo-theme-relearn#387 7 - don't empty search input if no data-val is given 8 - don't delete search term but close suggestions when suggestions are open 9 - delete search term when suggestions are closed 10 McShelby/hugo-theme-relearn#452 11 - register focus event ignoring minChars because that doesn't make sense 12 McShelby/hugo-theme-relearn#452 13 - on ESC, close overlay without deleting search term if overlay is open 14 - on ESC, delete search term if overlay is closed 15 - on UP, preventDefault to keep cursor in position 16 17 Copyright (c) 2014 Simon Steinberger / Pixabay 18 GitHub: https://github.com/Pixabay/JavaScript-autoComplete 19 License: http://www.opensource.org/licenses/mit-license.php 20 */ 21 22 var autoComplete = (function(){ 23 // "use strict"; 24 function autoComplete(options){ 25 if (!document.querySelector) return; 26 27 // helpers 28 function hasClass(el, className){ return el.classList ? el.classList.contains(className) : new RegExp('\\b'+ className+'\\b').test(el.className); } 29 30 function addEvent(el, type, handler){ 31 if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler); 32 } 33 function removeEvent(el, type, handler){ 34 // if (el.removeEventListener) not working in IE11 35 if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler); 36 } 37 function live(elClass, event, cb, context){ 38 addEvent(context || document, event, function(e){ 39 var found, el = e.target || e.srcElement; 40 while (el && !(found = hasClass(el, elClass))) el = el.parentElement; 41 if (found) cb.call(el, e); 42 }); 43 } 44 45 var o = { 46 selector: 0, 47 source: 0, 48 minChars: 3, 49 delay: 150, 50 offsetLeft: 0, 51 offsetTop: 1, 52 cache: 1, 53 menuClass: '', 54 selectorToInsert: 0, 55 renderItem: function (item, search){ 56 // escape special characters 57 search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 58 var re = new RegExp("(" + search.split(' ').join('|') + ")", "gi"); 59 return '<div class="autocomplete-suggestion" data-val="' + item + '">' + item.replace(re, "<b>$1</b>") + '</div>'; 60 }, 61 onSelect: function(e, term, item){} 62 }; 63 for (var k in options) { if (options.hasOwnProperty(k)) o[k] = options[k]; } 64 65 // init 66 var elems = typeof o.selector == 'object' ? [o.selector] : document.querySelectorAll(o.selector); 67 for (var i=0; i<elems.length; i++) { 68 var that = elems[i]; 69 70 // create suggestions container "sc" 71 that.sc = document.createElement('div'); 72 that.sc.className = 'autocomplete-suggestions '+o.menuClass; 73 74 that.autocompleteAttr = that.getAttribute('autocomplete'); 75 that.setAttribute('autocomplete', 'off'); 76 that.cache = {}; 77 that.last_val = ''; 78 79 var parentElement; 80 if (typeof o.selectorToInsert === "string" && document.querySelector(o.selectorToInsert) instanceof HTMLElement) { 81 parentElement = document.querySelector(o.selectorToInsert); 82 } 83 that.updateSC = function(resize, next){ 84 var rect = that.getBoundingClientRect(); 85 var parentOffsetLeft = 0; 86 var parentOffsetTop = 0; 87 var pageXOffset = 0; 88 var pageYOffset = 0; 89 if (parentElement != false) { 90 parentOffsetLeft = parentElement.getBoundingClientRect().left; 91 parentOffsetTop = parentElement.getBoundingClientRect().top; 92 } else { 93 pageXOffset = window.pageXOffset || document.documentElement.scrollLeft; 94 pageYOffset = window.pageYOffset || document.documentElement.scrollTop; 95 } 96 // Is this really the job of the tool or should it be defered to the user? 97 // that.sc.style.left = Math.round(rect.left + pageXOffset + o.offsetLeft - parentOffsetLeft) + 'px'; 98 // that.sc.style.top = Math.round(rect.bottom + pageYOffset + o.offsetTop - parentOffsetTop) + 'px'; 99 // that.sc.style.width = Math.round(rect.right - rect.left) + 'px'; // outerWidth 100 if (!resize) { 101 that.sc.style.display = 'block'; 102 if (!that.sc.maxHeight) { that.sc.maxHeight = parseInt((window.getComputedStyle ? getComputedStyle(that.sc, null) : that.sc.currentStyle).maxHeight); } 103 if (!that.sc.suggestionHeight) that.sc.suggestionHeight = that.sc.querySelector('.autocomplete-suggestion').offsetHeight; 104 if (that.sc.suggestionHeight) 105 if (!next) that.sc.scrollTop = 0; 106 else { 107 var scrTop = that.sc.scrollTop, selTop = next.getBoundingClientRect().top - that.sc.getBoundingClientRect().top; 108 if (selTop + that.sc.suggestionHeight - that.sc.maxHeight > 0) 109 that.sc.scrollTop = selTop + that.sc.suggestionHeight + scrTop - that.sc.maxHeight; 110 else if (selTop < 0) 111 that.sc.scrollTop = selTop + scrTop; 112 } 113 } 114 } 115 addEvent(window, 'resize', that.updateSC); 116 117 if (typeof o.selectorToInsert === "string" && document.querySelector(o.selectorToInsert) instanceof HTMLElement) { 118 document.querySelector(o.selectorToInsert).appendChild(that.sc); 119 } else { 120 document.body.appendChild(that.sc); 121 } 122 123 live('autocomplete-suggestion', 'mouseleave', function(e){ 124 var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); 125 if (sel) setTimeout(function(){ sel.className = sel.className.replace('selected', ''); }, 20); 126 }, that.sc); 127 128 live('autocomplete-suggestion', 'mouseover', function(e){ 129 var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); 130 if (sel) sel.className = sel.className.replace('selected', ''); 131 this.className += ' selected'; 132 }, that.sc); 133 134 live('autocomplete-suggestion', 'mousedown', function(e){ 135 if (hasClass(this, 'autocomplete-suggestion')) { // else outside click 136 var v = this.getAttribute('data-val'); 137 that.value = v; 138 o.onSelect(e, v, this); 139 that.sc.style.display = 'none'; 140 } 141 }, that.sc); 142 143 that.blurHandler = function(){ 144 try { var over_sb = document.querySelector('.autocomplete-suggestions:hover'); } catch(e){ var over_sb = 0; } 145 if (!over_sb) { 146 that.last_val = that.value; 147 that.sc.style.display = 'none'; 148 setTimeout(function(){ that.sc.style.display = 'none'; }, 350); // hide suggestions on fast input 149 } else if (that !== document.activeElement) setTimeout(function(){ that.focus(); }, 20); 150 }; 151 addEvent(that, 'blur', that.blurHandler); 152 153 var suggest = function(data){ 154 var val = that.value; 155 that.cache[val] = data; 156 if (data.length && val.length >= o.minChars) { 157 var s = ''; 158 for (var i=0;i<data.length;i++) s += o.renderItem(data[i], val); 159 that.sc.innerHTML = s; 160 that.updateSC(0); 161 } 162 else 163 that.sc.style.display = 'none'; 164 } 165 166 that.keydownHandler = function(e){ 167 var key = window.event ? e.keyCode : e.which; 168 // down (40), up (38) 169 if ((key == 40 || key == 38) && that.sc.innerHTML) { 170 e.preventDefault(); 171 var next, sel = that.sc.querySelector('.autocomplete-suggestion.selected'); 172 if (!sel) { 173 next = (key == 40) ? that.sc.querySelector('.autocomplete-suggestion') : that.sc.childNodes[that.sc.childNodes.length - 1]; // first : last 174 next.className += ' selected'; 175 if (next.getAttribute('data-val')) that.value = next.getAttribute('data-val'); 176 } else { 177 next = (key == 40) ? sel.nextSibling : sel.previousSibling; 178 if (next) { 179 sel.className = sel.className.replace('selected', ''); 180 next.className += ' selected'; 181 if (next.getAttribute('data-val')) that.value = next.getAttribute('data-val'); 182 } 183 else { 184 sel.className = sel.className.replace('selected', ''); 185 that.value = that.last_val; 186 next = 0; 187 } 188 } 189 that.updateSC(0, next); 190 return false; 191 } 192 // esc 193 else if (key == 27) { 194 if (that.sc.style.display != 'none') { 195 // just close the overlay if it's open, and prevent other listeners 196 // from recognizing it; this is not for you! 197 e.preventDefault(); 198 e.stopImmediatePropagation(); 199 that.sc.style.display = 'none'; 200 var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); 201 if (sel) { 202 that.focus(); 203 } 204 } 205 else { 206 // if no overlay is open, we want to remove the search term and also 207 // want other listeners to recognize it 208 that.value = ''; 209 } 210 } 211 // enter 212 else if (key == 13 || key == 9) { 213 var sel = that.sc.querySelector('.autocomplete-suggestion.selected'); 214 if (sel && that.sc.style.display != 'none') { o.onSelect(e, sel.getAttribute('data-val'), sel); setTimeout(function(){ that.sc.style.display = 'none'; }, 20); } 215 } 216 }; 217 addEvent(that, 'keydown', that.keydownHandler); 218 219 that.keyupHandler = function(e){ 220 var key = window.event ? e.keyCode : e.which; 221 if (!key || (key < 35 || key > 40) && key != 13 && key != 27) { 222 var val = that.value; 223 if (val.length >= o.minChars) { 224 if (val != that.last_val) { 225 that.last_val = val; 226 clearTimeout(that.timer); 227 if (o.cache) { 228 if (val in that.cache) { suggest(that.cache[val]); return; } 229 // no requests if previous suggestions were empty 230 for (var i=1; i<val.length-o.minChars; i++) { 231 var part = val.slice(0, val.length-i); 232 if (part in that.cache && !that.cache[part].length) { suggest([]); return; } 233 } 234 } 235 that.timer = setTimeout(function(){ o.source(val, suggest) }, o.delay); 236 } 237 } else { 238 that.last_val = val; 239 that.sc.style.display = 'none'; 240 } 241 } 242 }; 243 addEvent(that, 'keyup', that.keyupHandler); 244 245 that.focusHandler = function(e){ 246 that.last_val = '\n'; 247 that.keyupHandler(e) 248 }; 249 addEvent(that, 'focus', that.focusHandler); 250 } 251 252 // public destroy method 253 this.destroy = function(){ 254 for (var i=0; i<elems.length; i++) { 255 var that = elems[i]; 256 removeEvent(window, 'resize', that.updateSC); 257 removeEvent(that, 'blur', that.blurHandler); 258 removeEvent(that, 'focus', that.focusHandler); 259 removeEvent(that, 'keydown', that.keydownHandler); 260 removeEvent(that, 'keyup', that.keyupHandler); 261 if (that.autocompleteAttr) 262 that.setAttribute('autocomplete', that.autocompleteAttr); 263 else 264 that.removeAttribute('autocomplete'); 265 try { 266 if (o.selectorToInsert && document.querySelector(o.selectorToInsert).contains(that.sc)) { 267 document.querySelector(o.selectorToInsert).removeChild(that.sc); 268 } else { 269 document.body.removeChild(that.sc); 270 } 271 } catch (error) { 272 console.log('Destroying error: can\'t find target selector', error); 273 throw error; 274 } 275 that = null; 276 } 277 }; 278 } 279 return autoComplete; 280 })(); 281 282 (function(){ 283 if (typeof define === 'function' && define.amd) 284 define('autoComplete', function () { return autoComplete; }); 285 else if (typeof module !== 'undefined' && module.exports) 286 module.exports = autoComplete; 287 else 288 window.autoComplete = autoComplete; 289 })();