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  })();