jQuery autocomplete

WIP

by dumptyd

HTML

<link rel="stylesheet" href="https://bootswatch.com/cosmo/bootstrap.min.css">
<div class="row gutter-top-50">
  <div class="col-xs-2"></div>
  <div class="col-xs-8">
    <input id="auto-field-1" placeholder="Enter text" type="text" class="form-control">
    <input id="auto-field-2" placeholder="Enter text" type="text" class="form-control gutter-top-40">
  </div>
  <div class="col-xs-2"></div>
</div>

SCSS

body {
  background-color: #F3F5F6;
  overflow-x: hidden;
}


@for $i from 1 through 10 {
  $num: $i * 5;
  .gutter-top-$num {
    margin-top: $num + 'px';
  }
}

JavaScript

$.fn.autocomplete = function({data, name, maxResults=Infinity, key}) {
	if (!data || !data.length) throw('autocomplete needs data attribute');
 	let internalData = JSON.parse(JSON.stringify(data));
  if (key)
  	internalData = internalData.map(d => d[key]);
  let textfield = $(this);
  
  // define names
  let container = `autocomplete-${name}`;
  let listgroup = `autocomplete-list-group-${name}`;
  let idxDataAttr = `autocomplete=list-idx`;
  let listContainer = $('<ul>').addClass(`list-group ${listgroup}`).css({position: 'absolute', width: textfield.outerWidth()});
  
  // define some variables for maintaining state
  let activeIndex = -1;
  let listElements = [];
  
  
  // define event handlers and functions
  let changeSelectedItem = function(oldActiveIndex, force) {
  	let len = listElements.length;
  	if(activeIndex < 0 && !force) activeIndex = (len - 1);
    else if(activeIndex >= len && !force) activeIndex = 0;
    if(~oldActiveIndex)
      listElements[oldActiveIndex].removeClass('active');
    if(~activeIndex)
      listElements[activeIndex].addClass('active');
    if(force) {
      $(`.${listgroup}`).html('');
      listElements = [];
    }
  };
  let itemClickHandler = function() {
  	let oldActiveIndex = activeIndex;
  	activeIndex = Number($(this).data(idxDataAttr));
    textfield.val(listElements[activeIndex].text());
    activeIndex = -1;
  	changeSelectedItem(oldActiveIndex, true);  
  };
  
  // add some html
  $(this).wrap(`<div class="${container}"><div>`)
  $(`.${container}`).append(listContainer);

  // add events
  $(this).on('input', function() {
  	let val = $(this).val();
    
		let filtered = val ? internalData.filter(str => str.toLowerCase().includes(val.toLowerCase())).slice(0, maxResults) : [];    
    listElements = filtered.map((f, idx) => $('<a>').addClass('list-group-item').text(f).data(idxDataAttr, idx).click(itemClickHandler));
		
		$(`.${listgroup}`).html('');
    listElements.forEach(l => $(`.${listgroup}`).append(l));
   ...