Skip to content

Instantly share code, notes, and snippets.

@taufik-nurrohman
Last active June 7, 2021 03:30
Show Gist options
  • Select an option

  • Save taufik-nurrohman/6cd2422017f7baf1c745addcabaf3981 to your computer and use it in GitHub Desktop.

Select an option

Save taufik-nurrohman/6cd2422017f7baf1c745addcabaf3981 to your computer and use it in GitHub Desktop.
A custom select box draft to be used on Mecha’s control panel extension.
<meta charset="utf-8">
<style>
.select {
position: relative;
background: white;
color: black;
border: 1px solid;
font: inherit;
display: inline-block;
vertical-align: middle;
width: 12em;
padding: .5em .75em;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.select,
.select * {
box-sizing: border-box;
}
.select[disabled],
.select.disabled {
color: gray;
cursor: not-allowed;
}
.select:focus {
border-color: blue;
box-shadow: 0 0 0 3px rgba(0, 0, 255, .25);
}
.select.js::after {
content: "";
width: 0;
height: 0;
border-top: 6px solid;
border-right: 5px solid transparent;
border-bottom: 0;
border-left: 5px solid transparent;
position: absolute;
top: 50%;
margin-top: -3px;
right: .5em;
pointer-events: none;
}
.select.js.open::after {
border-top: 0;
border-bottom: 6px solid;
}
.select.js > span {
display: block;
margin-right: 1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.select.js > span + span {
position: fixed;
z-index: 9999;
background: inherit;
border: inherit;
margin: -1px 0 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, .4);
overflow: auto;
display: none;
}
.select.js > span + span a {
display: block;
padding: .25em .5em;
font: inherit;
color: inherit;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.select.js > span + span a:hover,
.select.js > span + span a.focused {
background: blue;
color: white;
}
.select.js > span + span span {
display: block;
padding: .25em .5em;
}
.select.js > span + span span[title]::before {
content: attr(title);
display: block;
margin: 0 0 .25em;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.select.js > span + span span a {
padding-left: 1.5em;
margin: 0 -.5em;
}
.select.js > span + span a.disabled {
color: gray;
cursor: not-allowed;
}
.select.js > span + span a.disabled:hover,
.select.js > span + span a.disabled.focused {
background: gray;
color: white;
}
.select.js > span + span a.selected {
color: blue;
}
.select.js > span + span a.selected:hover,
.select.js > span + span a.selected.focused {
background: blue;
color: white;
}
.select.js.open > span + span {
display: block;
}
.select.select-source {
position: fixed;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
background: none;
border: 0;
outline: 0;
font-size: 0;
overflow: hidden;
opacity: 0;
}
</style>
<p>
<label>
<input checked onchange="this.checked ? setSelectBoxesFake() : letSelectBoxesFake();" type="checkbox">
<span>Enable/Disable Custom Select Box</span>
</label>
</p>
<hr>
<form method="get">
<p>
<input name="input-1" type="text">
</p>
<p>
<select class="select" name="select-1">
<option>Red</option>
<option value="1">Green</option>
<option value="2">Blue</option>
<option disabled>Disabled</option>
</select>
</p>
<p>
<select class="select" name="select-2">
<option value="#000">Black (not in group)</option>
<optgroup label="Group 1">
<option value="#fff">White (in group)</option>
</optgroup>
<optgroup label="Group 2">
<option>Red</option>
<option value="1">Green</option>
<option value="2" selected>Blue</option>
<option disabled>Disabled</option>
</optgroup>
<option value="3">A very very very very very very very very very very very long value.</option>
<option>Item 1</option>
<option>Item 2</option>
<option>Item 3</option>
<option>Item 4</option>
<option>Item 5</option>
<option>Item 6</option>
<option>Item 7</option>
<option>Item 8</option>
<option>Item 9</option>
</select>
<br>
<small>Description goes here.</small>
</p>
<p>
<select class="select" disabled name="select-3">
<option>Test 1</option>
<option selected>Test 2</option>
<option>Test 3</option>
</select>
</p>
<p>
<input name="input-2" type="text">
</p>
<p>
<button type="submit" name="button-1" value="true">Submit</button>
<button type="reset">Reset</button>
</p>
</form>
<script>
function trigger(node, eventName) {
node.dispatchEvent(new Event(eventName));
}
function letSelectBoxFake(selectBox) {
selectBox.removeEventListener('focus', onSelectBoxFocus);
selectBox.removeEventListener('change', onSelectBoxChange);
selectBox.removeEventListener('input', onSelectBoxInput);
selectBox.classList.remove('select-source');
let selectBoxFake = selectBox.nextElementSibling;
if (selectBoxFake.classList.contains('select') && selectBoxFake.classList.contains('js')) {
selectBoxFake.removeEventListener('blur', onSelectBoxFakeBlur);
selectBoxFake.removeEventListener('click', onSelectBoxFakeClick);
selectBoxFake.removeEventListener('focus', onSelectBoxFakeFocus);
selectBoxFake.removeEventListener('keydown', onSelectBoxFakeKeyDown);
selectBoxFake.textContent = "";
selectBoxFake.remove();
}
}
function setSelectBoxFake(selectBox) {
let container = D.createElement('span'),
value = D.createElement('span'),
valueCurrent = selectBox.value,
t = selectBox.title,
index = 0,
items = selectBox.children;
container.className = selectBox.className;
container.classList.add('js');
container.append(value);
if (selectBox.multiple) {
// TODO
}
if (selectBox.size > 1) {
// TODO
}
function setSelectBoxFakeOptions(item, options) {
function onSelectBoxFakeOptionClick(e) {
let t = this,
valuePrev = valueCurrent;
value.textContent = t.textContent;
valueCurrent = t.dataset.value;
container.querySelectorAll('a[data-index]').forEach(option => {
option.classList[valueCurrent === option.dataset.value ? 'add' : 'remove']('selected');
});
trigger(container, 'focus');
if (valueCurrent !== valuePrev) {
selectBox.value = valueCurrent;
trigger(selectBox, 'input');
}
e.preventDefault();
}
if ('optgroup' === item.tagName.toLowerCase()) {
let optgroup = D.createElement('span'),
items = item.children;
optgroup.title = item.label;
options.append(optgroup);
for (let i = 0, j = items.length; i < j; ++i) {
setSelectBoxFakeOptions(items[i], optgroup);
}
return;
}
let option = D.createElement('a'),
v = item.getAttribute('value'),
t = item.textContent;
option.tabIndex = -1;
option.textContent = t;
option.title = t;
option.dataset.index = index;
option.dataset.value = v = null !== v ? v : t;
if (item.hasAttribute('disabled')) {
option.classList.add('disabled');
} else {
option.addEventListener('click', onSelectBoxFakeOptionClick, false);
}
options.append(option);
if (v === valueCurrent) {
value.textContent = t;
option.classList.add('selected');
}
++index;
}
if (t) {
container.title = t;
}
if (selectBox.disabled) {
container.classList.add('disabled');
} else {
container.tabIndex = 0;
selectBox.tabIndex = -1;
container.addEventListener('blur', onSelectBoxFakeBlur, false);
container.addEventListener('click', onSelectBoxFakeClick, false);
container.addEventListener('focus', onSelectBoxFakeFocus, false);
container.addEventListener('keydown', onSelectBoxFakeKeyDown, false);
}
if (items.length) {
let options = D.createElement('span');
container.append(options);
for (let i = 0, j = items.length; i < j; ++i) {
setSelectBoxFakeOptions(items[i], options);
}
}
selectBox.addEventListener('focus', onSelectBoxFocus, false);
selectBox.addEventListener('change', onSelectBoxChange, false);
selectBox.addEventListener('input', onSelectBoxInput, false);
selectBox.parentNode.insertBefore(container, selectBox.nextElementSibling);
selectBox.classList.add('select-source');
}
function setSelectBoxFakeOptionsPosition(selectBoxFake) {
let {height, left, top, width} = selectBoxFake.getBoundingClientRect(),
selectBoxFakeOptions = selectBoxFake.children[1];
selectBoxFakeOptions.style.top = (top + height) + 'px';
selectBoxFakeOptions.style.left = left + 'px';
selectBoxFakeOptions.style.width = width + 'px';
selectBoxFakeOptions.style.maxHeight = (W.innerHeight - top - height) + 'px';
let option = selectBoxFakeOptions.querySelector('a[data-index].selected');
if (option) {
selectBoxFakeOptions.scrollTop = (option.offsetTop + option.offsetHeight) - selectBoxFakeOptions.offsetHeight;
}
}
function onSelectBoxFakeClickOutside(e) {
selectBoxesFake && selectBoxesFake.forEach(selectBoxFake => {
selectBoxFake !== selectBoxFakeClicked && selectBoxFake.classList.remove('open');
});
}
function onSelectBoxFakeBlur(e) {
if (!selectBoxFakeClicked) {
return;
}
let t = this,
selectBox = t.previousElementSibling;
if (!t.classList.contains('open') && selectBox.value !== selectBoxValue) {
trigger(selectBox, 'change');
}
selectBoxFakeClicked = null;
}
function onSelectBoxFakeClick(e) {
let t = this;
t.classList.toggle('open');
if (t.classList.contains('open')) {
setSelectBoxFakeOptionsPosition(t);
}
selectBoxFakeClicked = t;
}
function onSelectBoxFakeFocus() {
let t = this,
selectBox = t.previousElementSibling;
selectBoxValue = selectBox.value;
}
function onSelectBoxFakeKeyDown(e) {
let t = this,
key = e.key,
keyCode = e.keyCode,
selectBox = t.previousElementSibling,
index = selectBox.selectedIndex,
option = t.querySelector('a[data-index="' + index + '"]'),
open = t.classList.contains('open');
// console.log([key, keyCode]);
if ('ArrowDown' === key || 40 === keyCode) {
while (option = t.querySelector('a[data-index="' + (++index) + '"]')) {
if (!option.classList.contains('disabled')) {
break;
}
}
if (option) {
trigger(option, 'click');
t.classList[open ? 'add' : 'remove']('open');
}
e.preventDefault();
} else if ('ArrowUp' === key || 38 === keyCode) {
while (option = t.querySelector('a[data-index="' + (--index) + '"]')) {
if (!option.classList.contains('disabled')) {
break;
}
}
if (option) {
trigger(option, 'click');
t.classList[open ? 'add' : 'remove']('open');
}
e.preventDefault();
} else if ('End' === key || 35 === keyCode) {
index = selectBox.options.length;
while (option = t.querySelector('a[data-index="' + (--index) + '"]')) {
if (!option.classList.contains('disabled')) {
break;
}
}
if (option) {
trigger(option, 'click');
t.classList[open ? 'add' : 'remove']('open');
}
e.preventDefault();
} else if ('Enter' === key || 13 === keyCode) {
t.classList.toggle('open');
e.preventDefault();
} else if ('Escape' === key || 27 === keyCode) {
t.classList.remove('open');
// e.preventDefault();
} else if ('Home' === key || 36 === keyCode) {
index = -1;
while (option = t.querySelector('a[data-index="' + (++index) + '"]')) {
if (!option.classList.contains('disabled')) {
break;
}
}
if (option) {
trigger(option, 'click');
t.classList[open ? 'add' : 'remove']('open');
}
e.preventDefault();
} else if ('Tab' === key || 9 === keyCode) {
option && trigger(option, 'click');
t.classList.remove('open');
// e.preventDefault();
}
setSelectBoxFakeOptionsPosition(t);
}
function onSelectBoxChange(e) {
onSelectBoxInput.call(this, e);
}
function onSelectBoxFocus(e) {
trigger(this.nextElementSibling, 'focus');
}
function onSelectBoxInput() {
let t = this,
selectBoxFake = t.nextElementSibling,
selectBoxFakeOptions = selectBoxFake.querySelectorAll('a[data-index]');
for (let i = 0, j = selectBoxFakeOptions.length; i < j; ++i) {
if (t.value === selectBoxFakeOptions[i].dataset.value) {
trigger(selectBoxFakeOptions[i], 'click');
break;
}
}
}
function onSelectBoxFormReset() {
W.setTimeout(() => selectBoxes.length && selectBoxes.forEach(selectBox => trigger(selectBox, 'change')), 10);
}
function letSelectBoxesFakeDocument() {
D.removeEventListener('click', onSelectBoxFakeClickOutside);
}
function setSelectBoxesFakeDocument() {
D.addEventListener('click', onSelectBoxFakeClickOutside, false);
}
let D = document,
R = D.documentElement,
W = window,
selectBoxValue,
selectBoxFakeClicked,
selectBoxes,
selectBoxesFake;
function letSelectBoxesFake() {
if (!selectBoxesFake) {
return;
}
let form;
selectBoxes.forEach((selectBox, index) => {
if (0 === index) {
if (form = selectBox.form) {
form.removeEventListener('reset', onSelectBoxFormReset);
}
}
letSelectBoxFake(selectBox);
});
letSelectBoxesFakeDocument();
}
function setSelectBoxesFake() {
selectBoxes = D.querySelectorAll('.select');
let form;
selectBoxes.forEach((selectBox, index) => {
if (0 === index) {
if (form = selectBox.form) {
form.addEventListener('reset', onSelectBoxFormReset, false);
}
}
setSelectBoxFake(selectBox);
});
selectBoxesFake = D.querySelectorAll('.select.js');
setSelectBoxesFakeDocument();
}
setSelectBoxesFake(); // Enable!
</script>
<script>
// Test for native JavaScript event(s)
document.querySelectorAll('select').forEach(select => {
select.addEventListener('change', function() {
console.log('change: ' + JSON.stringify(this.value));
});
select.addEventListener('input', function() {
console.log('input: ' + JSON.stringify(this.value));
});
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment