1const DEFAULTS = { 2 threshold: 2, 3 maximumItems: 5, 4 highlightTyped: true, 5 highlightClass: 'text-primary', 6 label: 'label', 7 value: 'value', 8 showValue: false, 9 showValueBeforeLabel: false, 10}; 11 12class Autocomplete { 13 constructor(field, options) { 14 this.field = field; 15 this.options = Object.assign({}, DEFAULTS, options); 16 this.dropdown = null; 17 18 field.parentNode.classList.add('dropdown'); 19 field.setAttribute('data-bs-toggle', 'dropdown'); 20 field.classList.add('dropdown-toggle'); 21 22 const dropdown = ce(`<div class="dropdown-menu"></div>`); 23 if (this.options.dropdownClass) 24 dropdown.classList.add(this.options.dropdownClass); 25 26 insertAfter(dropdown, field); 27 28 this.dropdown = new bootstrap.Dropdown(field, this.options.dropdownOptions); 29 30 field.addEventListener('click', (e) => { 31 if (this.createItems() === 0) { 32 e.stopPropagation(); 33 this.dropdown.hide(); 34 } 35 }); 36 37 field.addEventListener('input', () => { 38 if (this.options.onInput) 39 this.options.onInput(this.field.value); 40 this.renderIfNeeded(); 41 }); 42 43 field.addEventListener('keydown', (e) => { 44 if (e.keyCode === 27) { 45 this.dropdown.hide(); 46 return; 47 } 48 if (e.keyCode === 40) { 49 this.dropdown._menu.children[0]?.focus(); 50 return; 51 } 52 }); 53 } 54 55 setData(data) { 56 this.options.data = data; 57 this.renderIfNeeded(); 58 } 59 60 renderIfNeeded() { 61 if (this.createItems() > 0) 62 this.dropdown.show(); 63 else 64 this.field.click(); 65 } 66 67 createItem(lookup, item) { 68 let label; 69 if (this.options.highlightTyped) { 70 const idx = removeDiacritics(item.label) 71 .toLowerCase() 72 .indexOf(removeDiacritics(lookup).toLowerCase()); 73 const className = Array.isArray(this.options.highlightClass) ? this.options.highlightClass.join(' ') 74 : (typeof this.options.highlightClass == 'string' ? this.options.highlightClass : ''); 75 label = item.label.substring(0, idx) 76 + `<span class="${className}">${item.label.substring(idx, idx + lookup.length)}</span>` 77 + item.label.substring(idx + lookup.length, item.label.length); 78 } else { 79 label = item.label; 80 } 81 82 if (this.options.showValue) { 83 if (this.options.showValueBeforeLabel) { 84 label = `${item.value} ${label}`; 85 } else { 86 label += ` ${item.value}`; 87 } 88 } 89 90 return ce(`<button type="button" class="dropdown-item" data-label="${item.label}" data-value="${item.value}">${label}</button>`); 91 } 92 93 createItems() { 94 const lookup = this.field.value; 95 if (lookup.length < this.options.threshold) { 96 this.dropdown.hide(); 97 return 0; 98 } 99 100 const items = this.field.nextSibling; 101 items.innerHTML = ''; 102 103 const keys = Object.keys(this.options.data); 104 105 let count = 0; 106 for (let i = 0; i < keys.length; i++) { 107 const key = keys[i]; 108 const entry = this.options.data[key]; 109 const item = { 110 label: this.options.label ? entry[this.options.label] : key, 111 value: this.options.value ? entry[this.options.value] : entry 112 }; 113 114 if (removeDiacritics(item.label).toLowerCase().indexOf(removeDiacritics(lookup).toLowerCase()) >= 0) { 115 items.appendChild(this.createItem(lookup, item)); 116 if (this.options.maximumItems > 0 && ++count >= this.options.maximumItems) 117 break; 118 } 119 } 120 121 this.field.nextSibling.querySelectorAll('.dropdown-item').forEach((item) => { 122 item.addEventListener('click', (e) => { 123 let dataLabel = e.target.getAttribute('data-label'); 124 let dataValue = e.target.getAttribute('data-value'); 125 126 this.field.value = dataLabel; 127 128 if (this.options.onSelectItem) { 129 this.options.onSelectItem({ 130 value: dataValue, 131 label: dataLabel 132 }); 133 } 134 135 this.dropdown.hide(); 136 }) 137 }); 138 139 return items.childNodes.length; 140 } 141} 142 143/** 144 * @param html 145 * @returns {Node} 146 */ 147function ce(html) { 148 let div = document.createElement('div'); 149 div.innerHTML = html; 150 return div.firstChild; 151} 152 153/** 154 * @param elem 155 * @param refElem 156 * @returns {*} 157 */ 158function insertAfter(elem, refElem) { 159 return refElem.parentNode.insertBefore(elem, refElem.nextSibling); 160} 161 162/** 163 * @param {String} str 164 * @returns {String} 165 */ 166function removeDiacritics(str) { 167 return str 168 .normalize('NFD') 169 .replace(/[\u0300-\u036f]/g, ''); 170}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.