diff options
-rw-r--r-- | README.md | 60 | ||||
-rw-r--r-- | autocomplete.js | 127 |
2 files changed, 187 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa67f9b --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# bootstrap-5-autocomplete + +This is a rewrite of https://github.com/Honatas/bootstrap-4-autocomplete for bootstrap v5. + +### Example + +```js +const ac = new Autocomplete(field, { + data: [{label: "I'm a label", value: 42}], + maximumItems: 5, + onSelectItem: ({label, value}) => { + console.log("user selected:", label, value); + } +}); + +// later, when you need to change the dataset + +ac.setData([ + {label: 'New York JFK', value: 'JFK'}, + {label: 'Moscow SVO', value: 'SVO'}, +]); +``` + +### Options + +Options is a JSON object with the following attributes (in alphabetical order): + +**data**: +The data from where autocomplete will lookup items to show. This data has to be an array of JSON objects. The format for every item in the array is: + + {"label": "This is a text", "value": 42} + +**dropdownOptions**: +It's the same options from Bootstrap's Dropdown, documented [here](https://getbootstrap.com/docs/5.0/components/dropdowns/#options). + +**dropdownClass**: +The class of the dropdown-menu element, which is the box that is displayed. Can take a string or an array of strings. + +**highlightClass**: +The class to use when highlighting typed text on items. Only used when highlightTyped is true. Default is text-primary. Can take a string or an array of strings. + +**highlightTyped**: +Wether to highlight (style) typed text on items. Default is true. + +**maximumItems**: +How many items you want to show when the autocomplete is displayed. Default is 5. Set to 0 to display all available items. + +**onInput** + +**onSelectItem**: +A callback that is fired every time an item is selected. It receives an object in following format: + + {label: <label>, value: <value>} + +**treshold**: +The number of characters that need to be typed on the input in order to trigger the autocomplete. Default is 4. + +### License + +MIT
\ No newline at end of file diff --git a/autocomplete.js b/autocomplete.js new file mode 100644 index 0000000..c336123 --- /dev/null +++ b/autocomplete.js @@ -0,0 +1,127 @@ +const DEFAULTS = { + treshold: 2, + maximumItems: 5, + highlightTyped: true, + highlightClass: 'text-primary', +}; + +class Autocomplete { + constructor(field, options) { + this.field = field; + this.options = Object.assign({}, DEFAULTS, options); + this.dropdown = null; + + field.parentNode.classList.add('dropdown'); + field.setAttribute('data-toggle', 'dropdown'); + field.classList.add('dropdown-toggle'); + + const dropdown = ce(`<div class="dropdown-menu" ></div>`); + if (this.options.dropdownClass) + dropdown.classList.add(this.options.dropdownClass); + + insertAfter(dropdown, field); + + this.dropdown = new bootstrap.Dropdown(field, this.options.dropdownOptions) + + field.addEventListener('click', (e) => { + if (this.createItems() === 0) { + e.stopPropagation(); + this.dropdown.hide(); + } + }); + + field.addEventListener('input', () => { + if (this.options.onInput) + this.options.onInput(this.field.value); + this.renderIfNeeded(); + }); + + field.addEventListener('keydown', (e) => { + if (e.keyCode === 27) { + this.dropdown.hide(); + return; + } + }); + } + + setData(data) { + this.options.data = data; + } + + renderIfNeeded() { + if (this.createItems() > 0) + this.dropdown.show(); + else + this.field.click(); + } + + createItem(lookup, item) { + let label; + if (this.options.highlightTyped) { + const idx = item.label.toLowerCase().indexOf(lookup.toLowerCase()); + const className = Array.isArray(this.options.highlightClass) ? this.options.highlightClass.join(' ') + : (typeof this.options.highlightClass == 'string' ? this.options.highlightClass : '') + label = item.label.substring(0, idx) + + `<span class="${className}">${item.label.substring(idx, idx + lookup.length)}</span>` + + item.label.substring(idx + lookup.length, item.label.length); + } else + label = item.label; + return ce(`<button type="button" class="dropdown-item" data-value="${item.value}">${label}</button>`); + } + + createItems() { + const lookup = this.field.value; + if (lookup.length < this.options.treshold) { + this.dropdown.hide(); + return 0; + } + + const items = this.field.nextSibling; + items.innerHTML = ''; + + let count = 0; + for (let i = 0; i < this.options.data.length; i++) { + const {label, value} = this.options.data[i]; + const item = {label, value}; + if (item.label.toLowerCase().indexOf(lookup.toLowerCase()) >= 0) { + items.appendChild(this.createItem(lookup, item)); + if (this.options.maximumItems > 0 && ++count >= this.options.maximumItems) + break; + } + } + + this.field.nextSibling.querySelectorAll('.dropdown-item').forEach((item) => { + item.addEventListener('click', (e) => { + let dataValue = e.target.getAttribute('data-value'); + this.field.value = e.target.innerText; + if (this.options.onSelectItem) + this.options.onSelectItem({ + value: e.target.value, + label: e.target.innerText, + }); + this.dropdown.hide(); + }) + }); + + return items.childNodes.length; + } +} + +/** + * @param html + * @returns {Node} + */ +function ce(html) { + let div = document.createElement('div'); + div.innerHTML = html; + return div.firstChild; +} + +/** + * @param elem + * @param refElem + * @returns {*} + */ +function insertAfter(elem, refElem) { + return refElem.parentNode.insertBefore(elem, refElem.nextSibling) +}
\ No newline at end of file |