Skip to content

Instantly share code, notes, and snippets.

@unscriptable
Last active February 12, 2024 00:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save unscriptable/b6e5775f878faf436475069a53dd44b0 to your computer and use it in GitHub Desktop.
Save unscriptable/b6e5775f878faf436475069a53dd44b0 to your computer and use it in GitHub Desktop.
Datalist polyfill

Polyfill for the datalist element and list attribute for Safari on macOS

<!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>
// 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