Last active
February 12, 2024 00:32
-
-
Save unscriptable/b6e5775f878faf436475069a53dd44b0 to your computer and use it in GitHub Desktop.
Datalist polyfill
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<script src="index.js"></script> | |
</head> | |
<body> | |
<input list="myDatalist"/> | |
<datalist id="myDatalist"> | |
<option value="foo" label="Foo Bar">Bar</option> | |
</datalist> | |
<input list="myDatalist2"/> | |
<div style="padding: 3em;"> | |
<datalist id="myDatalist2"> | |
<option value="oofoo" label="ooFoo Bar">Bar</option> | |
<option value="foo" label="Foo Bar">Bar</option> | |
<option value="nufoo" label="NuFoo Bar">Bar</option> | |
<option value="bar" label="Just Bar">who cares</option> | |
</datalist> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A polyfill for input.list = datalist | |
// TODO: don't show options that are disabled | |
// TODO: touch events | |
// TODO: deal with other input types (number, email, etc) | |
(function (document) { | |
var keyEnter = 13 | |
var keyEsc = 27 | |
var keyUp = 38 | |
var keyDown = 40 | |
var datalistClass = window.HTMLDataListElement | |
var hasDatalistElement | |
= datalistClass | |
&& document.createElement('datalist') instanceof datalistClass | |
var hasInputListAttr = 'list' in document.createElement('input') | |
var toArray | |
= typeof Array.from === 'function' | |
? Array.from | |
: (function (clone) { return function (arr) { return clone.apply(arr) } })(Array.prototype.slice) | |
if (!hasDatalistElement) { | |
window.HTMLDataListElement = function HTMLDataListElement () {} | |
HTMLDataListElement.prototype = Object.create(HTMLElement.prototype) | |
ensureOptionsProperty(HTMLDataListElement.prototype) | |
} | |
if (!hasInputListAttr) { | |
injectStylesheet() | |
document.addEventListener( | |
'focusin', | |
function (event) { | |
if (hasListAttr(event.target)) addDatalistFeature(event.target) | |
}, | |
true // This must happen before focusin on input! | |
) | |
} | |
function hasListAttr (input) { | |
return input instanceof HTMLInputElement && input.hasAttribute('list') | |
} | |
// Shims object/element to look like a datalist | |
function ensureOptionsProperty (obj) { | |
if (!('options' in obj)) { | |
Object.defineProperty( | |
obj, | |
'options', | |
{ | |
get: function () { return this.getElementsByTagName('option') }, | |
set: function (value) { /* TODO */ }, | |
enumerable: true, | |
configurable: true | |
} | |
) | |
} | |
return obj | |
} | |
// TODO: use top side if there's not enough room underneath input | |
// TODO: stop putting this in the local DOM. insert into datalist? | |
function positionElementAroundInput (el) { | |
el.style.position = 'absolute' | |
return function (input) { | |
el.style.top = input.offsetTop + input.offsetHeight + 'px' | |
el.style.left = input.offsetLeft + 'px' | |
el.style.width = input.offsetWidth + 'px' | |
input.list.appendChild(el) | |
return input | |
} | |
} | |
function isAlreadyOpenAndAssignedToInput (dd) { | |
return function (input) { | |
return dd.input === input && !!dd.parentNode | |
} | |
} | |
function removeElement (el) { | |
if (el.parentNode != null) el.parentNode.removeChild(el) | |
return el | |
} | |
function loadDropDownFromInput (dd) { | |
function addOption (option) { dd.options.add(option.cloneNode(true)) } | |
function clearOptions () { dd.options.length = 0 } | |
function queryAll () { return true } | |
return function (input) { | |
var list = input.list | |
// TODO: throw if list can't be found? | |
var options = list.options | |
var query | |
= input.value | |
? function (option) { return option.value.match(input.value) } | |
: queryAll | |
clearOptions() | |
toArray(options) | |
.filter(query) | |
.forEach(addOption) | |
} | |
} | |
function assignDropdownToInput (dd) { | |
return function (input) { | |
dd.input = input | |
} | |
} | |
function adjustDropdownByStep (dd) { | |
return function (change) { | |
dd.selectedIndex = Math.min(dd.options.length - 1, Math.max(0, dd.selectedIndex + change)) | |
} | |
} | |
function resizeDropdownToInput (dd) { | |
return function (input) { | |
dd.size = Math.min(10, dd.options.length) | |
} | |
} | |
function copyDropdownValueToInput (dd) { | |
return function (input) { | |
var option = dd.options[dd.selectedIndex] | |
// clicks can happen outside all options | |
if (option) input.value = option.value | |
return input | |
} | |
} | |
function handleDropdownClick (event) { | |
var dd = event.currentTarget | |
var input = dd.input | |
if (!input) return | |
// TODO: kinda messy | |
copyDropdownValueToInput(dd)(input) | |
removeElement(dropDownElement) | |
} | |
function handleDropdownMouseOut (event) { | |
if (event.target instanceof HTMLOptionElement) { | |
var option = event.target | |
option.selected = false | |
} | |
} | |
function handleDropdownMouseOver (event) { | |
if (event.target instanceof HTMLOptionElement) { | |
var option = event.target | |
option.selected = true | |
} | |
} | |
function createDropdownElement () { | |
var select = document.createElement('select') | |
select.multiple = true | |
select.size = 10 // to be overwritten when populated or positioned | |
select.tabIndex = -1 // remove from tabbed controls | |
return select | |
} | |
function addDatalistFeature (input) { | |
var datalist = document.getElementById(input.getAttribute('list')) | |
input.list = datalist && ensureOptionsProperty(datalist) | |
if (input.list != null) { | |
input.addEventListener('focusin', handleFocusin, false) | |
input.addEventListener('keydown', handleKeydown, false) | |
input.addEventListener('focusout', handleFocusout, false) | |
input.addEventListener('input', handleInput, false) | |
} | |
} | |
// The dropdown element is reused for all inputs | |
var dropDownElement = createDropdownElement() | |
var positionDropdown = positionElementAroundInput(dropDownElement) | |
var loadDropdown = loadDropDownFromInput(dropDownElement) | |
var assignDropdown = assignDropdownToInput(dropDownElement) | |
var resizeDropdown = resizeDropdownToInput(dropDownElement) | |
var isAlreadyOpenAndAssigned = isAlreadyOpenAndAssignedToInput(dropDownElement) | |
var adjustDropdownBy = adjustDropdownByStep(dropDownElement) | |
var copyDropdownValue = copyDropdownValueToInput(dropDownElement) | |
// TODO: consider adding these only when assigned to an input | |
dropDownElement.addEventListener('click', handleDropdownClick, false) | |
dropDownElement.addEventListener('mouseover', handleDropdownMouseOver, false) | |
dropDownElement.addEventListener('mouseout', handleDropdownMouseOut, false) | |
function handleFocusin (event) { | |
var input = event.target | |
loadDropdown(input) | |
resizeDropdown(input) | |
positionDropdown(input) | |
assignDropdown(input) | |
} | |
function handleFocusout (event) { | |
if (event.relatedTarget !== dropDownElement) | |
removeElement(dropDownElement) | |
} | |
function handleKeydown (event) { | |
var input = event.target | |
switch (event.keyCode) { | |
case keyEsc: | |
return removeElement(dropDownElement) | |
case keyEnter: | |
return isAlreadyOpenAndAssigned(input) | |
&& (copyDropdownValue(input), removeElement(dropDownElement)) | |
case keyDown: | |
return isAlreadyOpenAndAssigned(input) | |
? adjustDropdownBy(1) | |
: positionDropdown(input) | |
case keyUp: | |
return isAlreadyOpenAndAssigned(input) | |
? adjustDropdownBy(-1) | |
: positionDropdown(input) | |
} | |
} | |
function handleInput (event) { | |
var input = event.target | |
if (isAlreadyOpenAndAssigned(input)) { | |
loadDropdown(input) | |
} | |
} | |
function injectStylesheet () { | |
var head = document.head || document.body | |
var sheet = document.createElement('style') | |
var glyphCss | |
= 'input[list]:hover, input[list]:active {' | |
+ 'background-position: right; background-repeat: no-repeat; background-size: 12px 8px;' | |
+ 'background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 8 8"><text x="0" y="8">\u25BE</text></svg>\');' | |
+ '}' | |
sheet.appendChild(document.createTextNode(glyphCss)) | |
document.head.appendChild(sheet) | |
} | |
}(document)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment