214 lines
7.9 KiB
JavaScript
214 lines
7.9 KiB
JavaScript
|
// 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));
|