Last active
June 7, 2021 03:30
-
-
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.
This file contains hidden or 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
| <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 { | |
| 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> | |
| </p> | |
| </form> | |
| <script> | |
| function letSelectBoxFake(selectBox) { | |
| selectBox.removeEventListener('focus', onSelectBoxFocus); | |
| selectBox.classList.remove('select-source'); | |
| let selectBoxFake = selectBox.nextElementSibling; | |
| if (selectBoxFake.classList.contains('select') && selectBoxFake.classList.contains('js')) { | |
| selectBoxFake.removeEventListener('click', onSelectBoxFakeClick); | |
| 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, | |
| eventInput = new Event('input'), | |
| 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'); | |
| }); | |
| container.focus(); | |
| if (valueCurrent !== valuePrev) { | |
| selectBox.value = valueCurrent; | |
| selectBox.dispatchEvent(eventInput); | |
| } | |
| 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.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() { | |
| if (!selectBoxFakeClicked) { | |
| return; | |
| } | |
| let t = this, | |
| selectBox = t.previousElementSibling; | |
| if (!t.classList.contains('open') && selectBox.value !== selectBoxValue) { | |
| selectBox.dispatchEvent(new Event('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) { | |
| 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) { | |
| 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) { | |
| 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) { | |
| option.click(); | |
| t.classList[open ? 'add' : 'remove']('open'); | |
| } | |
| e.preventDefault(); | |
| } else if ('Tab' === key || 9 === keyCode) { | |
| option && option.click(); | |
| t.classList.remove('open'); | |
| // e.preventDefault(); | |
| } | |
| setSelectBoxFakeOptionsPosition(t); | |
| } | |
| function onSelectBoxFocus(e) { | |
| this.nextElementSibling.focus(); | |
| } | |
| 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; | |
| } | |
| selectBoxes.forEach(selectBox => letSelectBoxFake(selectBox)); | |
| letSelectBoxesFakeDocument(); | |
| } | |
| function setSelectBoxesFake() { | |
| selectBoxes = D.querySelectorAll('.select'); | |
| selectBoxes.forEach(selectBox => setSelectBoxFake(selectBox)); | |
| selectBoxesFake = D.querySelectorAll('.select.js'); | |
| setSelectBoxesFakeDocument(); | |
| } | |
| setSelectBoxesFake(); // Enable! | |
| </script> | |
| <script> | |
| // Test for native change event | |
| 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