// https://github.com/Fyrd/purejs-datalist-polyfill // license: MIT (function(document) { var IE_SELECT_ATTRIBUTE = 'data-datalist'; var LIST_CLASS = 'datalist-polyfill'; var ACTIVE_CLASS = 'datalist-polyfill__active'; var datalistSupported = !!(document.createElement('datalist') && window.HTMLDataListElement); var ua = navigator.userAgent; // Android does not have actual support var isAndroidBrowser = ua.match(/Android/) && !ua.match(/(Firefox|Chrome|Opera|OPR)/); if( datalistSupported && !isAndroidBrowser ) { return; } var inputs = document.querySelectorAll('input[list]'); var triggerEvent = function(elem, eventType) { var event; if (document.createEvent) { event = document.createEvent("HTMLEvents"); event.initEvent(eventType, true, true); elem.dispatchEvent(event); } else { event = document.createEventObject(); event.eventType = eventType; elem.fireEvent("on" + eventType, event); } }; for( var i = 0; i < inputs.length; i++ ) { var input = inputs[i]; var listId = input.getAttribute('list'); var datalist = document.getElementById(listId); if( !datalist ) { console.error('No datalist found for input: ' + listId); return; } // Only visible to <= IE9 var childSelect = document.querySelector('select[' + IE_SELECT_ATTRIBUTE + '="' + listId + '"]'); var parent = childSelect || datalist; var listItems = parent.getElementsByTagName('option'); convert(input, datalist, listItems); if( childSelect ) { childSelect.parentNode.removeChild( childSelect ); } } function convert(input, datalist, listItems) { var fakeList = document.createElement('ul'); var visibleItems = null; fakeList.id = listId; fakeList.className = LIST_CLASS; document.body.appendChild( fakeList ); var scrollValue = 0; // Used to prevent reflow var tempItems = document.createDocumentFragment(); for( var i = 0; i < listItems.length; i++ ) { var item = listItems[i]; var li = document.createElement('li'); li.innerText = item.value; tempItems.appendChild( li ); } fakeList.appendChild( tempItems ); var fakeItems = fakeList.childNodes; var eachItem = function(callback) { for( var i = 0; i < fakeItems.length; i++ ) { callback(fakeItems[i]); } }; var listen = function(elem, event, func) { if( elem.addEventListener ) { elem.addEventListener(event, func, false); } else { elem.attachEvent('on' + event, func); } }; datalist.parentNode.removeChild( datalist ); listen(input, 'focus', function() { // Reset scroll fakeList.scrollTop = 0; scrollValue = 0; }); listen(input, 'blur', function(evt) { // If this fires immediately, it prevents click-to-select from working setTimeout(function() { fakeList.style.display = 'none'; eachItem( function(item) { // Note: removes all, not just ACTIVE_CLASS, but should be safe item.className = ''; }); }, 100); }); var positionList = function() { fakeList.style.top = input.offsetTop + input.offsetHeight + 'px'; fakeList.style.left = input.offsetLeft + 'px'; fakeList.style.width = input.offsetWidth + 'px'; }; var itemSelected = function(item) { input.value = item.innerText; triggerEvent(input, 'change'); setTimeout(function() { fakeList.style.display = 'none'; }, 100); }; var buildList = function(e) { // Build datalist fakeList.style.display = 'block'; positionList(); visibleItems = []; eachItem( function(item) { // Note: removes all, not just ACTIVE_CLASS, but should be safe var query = input.value.toLowerCase(); var isFound = query.length && item.innerText.toLowerCase().indexOf( query ) > -1; if( isFound ) { visibleItems.push( item ); } item.style.display = isFound ? 'block' : 'none'; } ); }; listen(input, 'keyup', buildList); listen(input, 'focus', buildList); // Don't want to use :hover in CSS so doing this instead // really helps with arrow key navigation eachItem( function(item) { // Note: removes all, not just ACTIVE_CLASS, but should be safe listen(item, 'mouseover', function(evt) { eachItem( function(_item) { _item.className = item == _item ? ACTIVE_CLASS : ''; }); }); listen(item, 'mouseout', function(evt) { item.className = ''; }); // Mousedown fires before native 'change' event is triggered // So we use this instead of click so only the new value is passed to 'change' listen(item, 'mousedown', function(evt) { itemSelected(item); }); }); listen(window, 'resize', positionList); listen(input, 'keydown', function(e) { var activeItem = fakeList.querySelector("." + ACTIVE_CLASS); if( !visibleItems.length ) { return; } var lastVisible = visibleItems[ visibleItems.length-1 ]; var datalistItemsHeight = lastVisible.offsetTop + lastVisible.offsetHeight; // up/down arrows var isUp = e.keyCode == 38; var isDown = e.keyCode == 40; if ( (isUp || isDown) ) { if( isDown && !activeItem ) { visibleItems[0].className = ACTIVE_CLASS; } else if (activeItem) { var prevVisible = null; var nextVisible = null; for( var i = 0; i < visibleItems.length; i++ ) { var visItem = visibleItems[i]; if( visItem == activeItem ) { prevVisible = visibleItems[i-1]; nextVisible = visibleItems[i+1]; break; } } activeItem.className = ''; if ( isUp ) { if( prevVisible ) { prevVisible.className = ACTIVE_CLASS; if ( prevVisible.offsetTop < fakeList.scrollTop ) { fakeList.scrollTop -= prevVisible.offsetHeight; } } else { visibleItems[visibleItems.length - 1].className = ACTIVE_CLASS; } } if ( isDown ) { if( nextVisible ) { nextVisible.className = ACTIVE_CLASS; if( nextVisible.offsetTop + nextVisible.offsetHeight > fakeList.scrollTop + fakeList.offsetHeight ) { fakeList.scrollTop += nextVisible.offsetHeight; } } else { visibleItems[0].className = ACTIVE_CLASS; } } } } // return or tab key if ( activeItem && (e.keyCode == 13 || e.keyCode == 9) ){ itemSelected(activeItem); } }); } }(document));