Skip to content

Instantly share code, notes, and snippets.

@spin0us
Last active April 1, 2022 15:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save spin0us/fb81680d5761bfb810446005b2fe6e6c to your computer and use it in GitHub Desktop.
Save spin0us/fb81680d5761bfb810446005b2fe6e6c to your computer and use it in GitHub Desktop.
A pure JavaScript combobox
/*!
* A pure JavaScript combobox v1.3
* https://gist.github.com/spin0us/fb81680d5761bfb810446005b2fe6e6c
*
* Author: Spin0us
*
* Date: 2022-04-01
*/
'use strict';
document.querySelectorAll('select[data-search="true"]').forEach(elm => {
const prefix = 'cust';
const width = elm.offsetWidth;
// Define style if not already defined
if(!document.querySelector(`style#${prefix}Combobox`)){
const styleSheet = document.createElement('style');
styleSheet.id = prefix+"Comboxbox";
styleSheet.type = "text/css";
styleSheet.innerHTML = `
.${prefix}SelectWrapper
{ display:inline-block; padding:0; margin:0; }
.${prefix}SelectWrapper>button
{ display:grid; grid-template-columns:1fr auto; align-items:center; cursor:pointer; border:1px solid #aaa; border-radius:.25rem; padding:0; text-align:left; }
.${prefix}SelectWrapper>button input
{ background-color:#fff; margin:0; padding:1px .5rem; border:0; border-right:1px solid #aaa; outline:none; }
.${prefix}SelectWrapper.active>button input
{ background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Ccircle cx='11.8' cy='11.8' r='10' fill='none' stroke='%23ddd' stroke-width='3.5'/%3E%3Cline x1='19.5' y1='19.5' x2='27.5' y2='27.5' stroke='%23ddd' stroke-width='5' stroke-linecap='round'/%3E%3C/svg%3E"); background-position: calc(100% - 2px) center; background-repeat: no-repeat; background-size: auto 90%; padding-right: 20px;}
.${prefix}SelectWrapper>button small
{ color:#555; font-size:.6rem; padding:1px .25rem; }
.${prefix}SelectWrapper>ul
{ display:none; position:absolute; margin:0; padding:0; background-color:#fff; max-height:10rem; overflow-y:auto; border:1px solid #aaa; border-top:0; }
.${prefix}SelectWrapper.active>ul
{ display:inherit !important; }
.${prefix}SelectWrapper>ul>li
{ cursor:pointer; list-style-type:none; padding:.25rem .5rem; font-family:Tahoma; font-size:.8rem; -ms-user-select:none; -moz-user-select:none; -webkit-user-select:none; -webkit-touch-callout: none; -khtml-user-select: none; user-select:none; }
.${prefix}SelectWrapper>ul>li[data-selected="true"]
{ background-color:#d9e0ff; }
.${prefix}SelectWrapper>ul>li:hover
{ background-color:#39f; color:#fff; }
`;
document.head.appendChild(styleSheet);
}
// Create wrapper element
const spn = document.createElement('span');
spn.id = elm.id;
spn.classList.add(`${prefix}SelectWrapper`);
spn.innerHTML = '<button type="button"><input type="text" value=""/><small>▼</small></button><ul></ul>';
spn.value = elm.options[0].value;
spn.dataset.value = elm.options[0].value;
spn.dataset.text = elm.options[0].textContent;
if(elm.attributes.onchange) spn.setAttribute(elm.attributes.onchange.nodeName, elm.attributes.onchange.nodeValue);
// Replace select by the wrapper
elm.parentNode.replaceChild(spn,elm);
// Retreive components
const btn = document.querySelector(`#${elm.id} button`);
const inp = document.querySelector(`#${elm.id} input`);
const sml = document.querySelector(`#${elm.id} small`);
const lst = document.querySelector(`#${elm.id} ul`);
// Resize button
btn.style.width = (width + document.querySelector(`#${elm.id} small`).offsetWidth + 2) + 'px';
// Create select list
lst.style.width = width + 'px';
// Build list content based on options
for(let i=0; i<elm.options.length; i++){
let li = document.createElement('li');
li.dataset.value = elm.options[i].value;
li.innerHTML = elm.options[i].textContent;
li.classList = elm.options[i].classList;
lst.appendChild(li);
if(elm.options[i].hasAttribute('selected')){
li.dataset.selected = true;
spn.value = elm.options[i].value;
spn.dataset.text = elm.options[i].textContent;
}
}
// Init. input
inp.value = spn.dataset.text;
inp.placeholder = spn.dataset.text;
const filter = function(val=''){
let rows = document.querySelectorAll(`#${elm.id} li`),
reg = RegExp(val, 'i');
rows.forEach( row => { row.style.display = reg.test(row.textContent.replace(/\s+/g, ' ')) ? '' : 'none'; });
};
// Manage selec events
const toggleActive = function(){
spn.classList.toggle('active');
if(spn.classList.contains('active')){
inp.value = '';
filter();
let li = document.querySelector(`#${elm.id} li[data-value="${spn.value}"]`);
lst.scrollTo(0,li.offsetTop);
setTimeout(function(){inp.focus()}, 50);
} else {
inp.value = spn.dataset.text;
inp.placeholder = spn.dataset.text;
if(spn.value != spn.dataset.value){
spn.dispatchEvent(new Event('change'));
spn.dataset.value = spn.value;
}
}
};
inp.addEventListener('keyup', function(){
filter(this.value.trim());
});
document.addEventListener('mousedown', function(e){
if(e.target && e.target.closest(`#${elm.id}`)){
if(e.target.nodeName == 'INPUT' && !spn.classList.contains('active')) toggleActive();
if(e.target.nodeName == 'SMALL') toggleActive();
if(e.target.nodeName == 'LI'){
document.querySelector(`#${elm.id} li[data-value="${spn.value}"]`).dataset.selected = false;
spn.value = e.target.dataset.value;
spn.dataset.text = e.target.textContent;
e.target.dataset.selected = true;
toggleActive();
}
}
else if(spn.classList.contains('active')) toggleActive();
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment