自定义select弹出面板:
根据按下的按键和option的内容定位到option、选中项左侧标记、自定义滚动条、上下键滚动列表,回车选定、事件委托
效果图:
//js调用:
initSelect(s); //s为select的id或select的dom对象
selectDemo.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>select</title>
<style>
body {
background: #999999;
}
#info {
height: 400px;
margin-bottom: 50px;
overflow-y: scroll;
}
.baseSelect {
height: 30px;
width: 120px;
background-color: #252833;
color: rgba(255, 255, 255, 0.692);
font-size: 16px;
border-radius: 3px;
border: none;
outline: none;
}
.selectPosition {
position: relative;
height: 0;
}
.selectPopupPanel {
position: absolute;
border-radius: 2px;
box-shadow: 1px 2px 10px 0 rgba(0, 0, 0, 0.5);
border: solid 1px rgba(213, 213, 213, 0.13);
background-color: #252833;
cursor: pointer;
outline: none;
display: none;
overflow: hidden;
opacity: 0;
transition: opacity .4s;
}
.selectPopupPanel .scrollBar {
position: absolute;
top: 0;
right: 0;
width: 4px;
background-color: transparent;
}
.selectPopupPanel .scrollBar .slidBlock {
position: absolute;
width: 100%;
border-radius: 2px;
background-color: #3d4351;
}
.selectPopupPanel .scrollBar .slidBlock:hover {
box-shadow: -1px 0 3px rgba(241, 243, 243, 0.5);
}
.selectPopupPanel .selectOptions {
overflow-x: hidden;
overflow-y: scroll;
background-color: #252833;
margin-right: -50px;
}
.selectPopupPanel .selectOptions .option {
width: 100%;
height: 42px;
background-color: transparent;
position: relative;
}
.selectPopupPanel .selectOptions .option .tag {
position: absolute;
width: 4px;
height: 100%;
}
.selectPopupPanel .selectOptions .optionSelected {
background-color: #343743;
}
.selectPopupPanel .selectOptions .optionSelected .tag {
background-image: linear-gradient(5deg, #ff5858 15%, #f857a6 80%);
}
.selectPopupPanel .selectOptions .optionFoucs {
background-color: #343743;
}
.selectPopupPanel .selectOptions .option .optionText {
position: absolute;
left: 4px;
right: 40px;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
line-height: 42px;
padding-left: 8px;
font-family: Roboto;
font-size: 16px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
letter-spacing: normal;
color: #b1b8c8;
user-select: none;
}
</style>
</head>
<body>
<div id="info">info:</div>
<div>
<input type="checkbox" id="c1" checked="checked">
<label for="c1">Use custom</label>
</div>
<div>item:</div>
<select class="baseSelect" id="s1">
<option value="January">January</option>
<option value="February">February</option>
<option value="March">March</option>
<option value="April">April</option>
<option value="May">May</option>
<option value="June">June</option>
<option value="July">July</option>
<option value="August">August</option>
<option value="September">September</option>
<option value="October">October</option>
<option value="November">November</option>
<option value="December">December</option>
<option value="January2">January2</option>
<option value="February2">February2</option>
<option value="March2">March2</option>
<option value="April2">April2</option>
<option value="May2">May2</option>
<option value="June2">June2</option>
<option value="July2">July2</option>
<option value="August2">August2</option>
<option value="September2">September2</option>
<option value="October2">October2</option>
<option value="November2">November2</option>
<option value="December2">December2</option>
</select>
<div style="margin-top: 600px;">footer</div>
<script>
function initSelect(value) {
let select = null;
if (typeof value == "string") {
select = document.getElementById(value);
} else {
select = value;
}
createPopupPanel();
let selectPopupPanel = select.parentNode.getElementsByClassName("selectPopupPanel")[0];
let selectCtrl = null;
let timeoutId = null;
let bodyOldOverflowY = null;
select.handles = addSelectListeners();
function addSelectListeners() {
let handles = {
"mousedown": function () {
this.mousedownTime = new Date().getTime();
this.mouseup = false;
},
"click": function () {
if (new Date().getTime() - this.mousedownTime < 500) {
timeoutId = showSelectPanel();
}
},
"keyup": function (e) {
e = e || arguments.callee.caller.arguments[0];
if (!e) {
return;
}
let keyCode = e.keyCode;
if (keyCode == 38 || keyCode == 40 || keyCode == 13) {
showSelectPanel();
}
}
};
select.addEventListener("mousedown", handles["mousedown"]);
select.addEventListener("click", handles["click"]);
select.addEventListener("keyup", handles["keyup"]);
return handles;
}
function showSelectPanel() {
if (selectPopupPanel.style.display == "block") {
return;
}
if (!selectCtrl) {
selectCtrl = initSelectList(select, selectPopupPanel);
}
return setTimeout(function () {
showInfo("show");
timeoutId = null;
selectPopupPanel.style.display = "block";
selectCtrl.calcRect();
selectCtrl.move(select.selectedIndex, true, true);
selectPopupPanel.style.opacity = 1;
bodyOldOverflowY = document.body.style.overflowY;
document.body.style.overflowY = "hidden";
selectPopupPanel.focus();
}, 50);
}
function hideSelectPanel(timeout) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
showInfo("hide");
if (selectPopupPanel.style.display == "none") {
return;
}
select.focus();
selectCtrl.reset();
selectPopupPanel.style.opacity = 0;
document.body.style.overflowY = bodyOldOverflowY;
if (timeout == 0) {
selectPopupPanel.style.display = "none";
}
setTimeout(function () {
selectPopupPanel.style.display = "none";
}, isNaN(timeout) ? 200 : timeout);
}
function createPopupPanel() {
select.insertAdjacentHTML("afterend",
'<div class="selectPanelPosition">' +
'<div class="selectPopupPanel" tabindex="9999">' +
' <div class="selectOptions"></div>' +
' <div class="scrollBar">' +
' <div class="slidBlock"></div>' +
' </div>' +
'</div></div>');
let options = select.options;
let optionsCount = options.length;
for (let i = 0; i < optionsCount; i++) {
options[i].style.display = "none";
}
}
function initSelectList(select, selectPopupPanel) {
let MAX_HEIGHT = Math.ceil(window.screen.availHeight * 0.618);
let options = select.options;
let optionsCount = options.length;
if (optionsCount == 0) {
return;
}
let optionTextArr = new Array(options.length);
let selectOptionsPanel = selectPopupPanel.getElementsByClassName("selectOptions")[0];
let divOptionsHtml = "";
let chars = {};
for (let i = 0; i < optionsCount; i++) {
divOptionsHtml +=
'<div data="' + options[i].value + '" class="option">' +
' <div class="tag"></div>' +
' <div class="optionText">' + options[i].text + '</div>' +
'</div>';
options[i].style.display = "none";
optionTextArr[i] = options[i].text.toUpperCase();
}
selectOptionsPanel.innerHTML = divOptionsHtml;
for (let i = 0; i < optionsCount; i++) {
let text = optionTextArr[i];
for (let j = 0; j < text.length; j++) {
let char = text[j];
if (chars[char] && chars[char].indexOf(i) === -1) {
chars[char].push(i);
} else {
chars[char] = [i];
}
}
}
let charsKeys = Object.keys(chars);
for (let i = 0; i < charsKeys.length; i++) {
let char = charsKeys[i];
chars[char].sort(function (a, b) {
return optionTextArr[a].indexOf(char) - optionTextArr[b].indexOf(char);
});
}
let scrollBar = selectPopupPanel.getElementsByClassName("scrollBar")[0]
let slidBlock = selectPopupPanel.getElementsByClassName("slidBlock")[0];
let divOptions = selectOptionsPanel.getElementsByClassName("option");
let panelHeight = 0;
let lastSelectedIndex = -1;
let lastFoucsIndex = -1;
let lastKeyCode = null;
let nextIndex = 0;
slidBlock.onmousedown = function (event) {
let shade = document.createElement("div");
shade.style.position = "fixed";
shade.style.left = "0";
shade.style.top = "0";
shade.style.right = "0";
shade.style.bottom = "0";
shade.style.zIndex = 999999;
document.body.appendChild(shade);
let slidBlockDrapY = event.clientY;
let top = Number.parseInt(slidBlock.style.marginTop);
let slidHeight = Number.parseInt(slidBlock.style.height);
let scorllBarRange = panelHeight - slidHeight;
let scorllRange = selectOptionsPanel.scrollHeight - scrollBar.getClientRects()[0].height;
let mousemoveHandle = function (e) {
let offset = e.clientY - slidBlockDrapY + top;
if (offset >= -10 && offset <= scorllBarRange + 10) {
selectOptionsPanel.scrollTop = Number.parseInt(1.0 * offset / scorllBarRange * scorllRange);
}
};
let releaseHandle = function () {
shade.removeEventListener("blur", this);
shade.removeEventListener("nouseup", this);
shade.removeEventListener("mousemove", mousemoveHandle);
shade.remove()
};
shade.addEventListener("blur", releaseHandle);
shade.addEventListener("mouseup", releaseHandle);
shade.addEventListener("mousemove", mousemoveHandle);
};
let moveFun = function (index, selected, moveScroll) {
if (index < 0 || index > optionsCount) {
index = 0;
}
if (moveScroll) {
let optionHeight = divOptions[0].clientHeight;
let optionY = index * optionHeight;
if (optionY < selectOptionsPanel.scrollTop) {
selectOptionsPanel.scrollTop = optionY;
} else if (optionY + optionHeight > selectOptionsPanel.scrollTop + panelHeight) {
selectOptionsPanel.scrollTop = optionY + optionHeight - panelHeight;
}
}
if (lastFoucsIndex >= 0 && lastFoucsIndex != lastSelectedIndex) {
divOptions[lastFoucsIndex].setAttribute("class", "option");
}
lastFoucsIndex = index;
if (selected) {
if (lastSelectedIndex >= 0) {
divOptions[lastSelectedIndex].setAttribute("class", "option");
}
divOptions[index].setAttribute("class", "option optionSelected");
lastSelectedIndex = index;
} else {
if (index != lastSelectedIndex) {
divOptions[index].setAttribute("class", "option optionFoucs");
}
}
};
let calcRect = function () {
let marginTop = 0;
let showScrollBar = false;
let selectBottom = select.getClientRects()[0].y + select.clientHeight;
let optionHeight = divOptions[0].clientHeight;
let windowHeight = window.innerHeight;
panelHeight = selectOptionsPanel.scrollHeight;
if (panelHeight > MAX_HEIGHT) {
panelHeight = MAX_HEIGHT;
showScrollBar = true;
}
if (windowHeight - 15 - selectBottom < panelHeight) {
if (selectBottom - select.clientHeight > windowHeight / 2) {
let maxHeight = selectBottom - select.clientHeight - optionHeight;
if (panelHeight > maxHeight) {
panelHeight = maxHeight;
showScrollBar = true;
}
marginTop = -panelHeight - select.clientHeight - 3;
} else {
panelHeight = windowHeight - selectBottom - optionHeight;
showScrollBar = true;
}
}
selectPopupPanel.style.width = select.clientWidth - 2 + "px";
selectPopupPanel.style.marginTop = marginTop + "px";
selectOptionsPanel.style.height = panelHeight + "px";
if (showScrollBar) {
scrollBar.style.display = "block";
scrollBar.style.height = panelHeight + "px";
let scrollHandle = function () {
let scale = 1.0 * panelHeight / selectOptionsPanel.scrollHeight;
let scrollBarHeight = panelHeight * scale;
let scrollBarTop = selectOptionsPanel.scrollTop * scale;
slidBlock.style.marginTop = scrollBarTop + "px";
slidBlock.style.height = scrollBarHeight + "px";
};
selectOptionsPanel.scrollTop = 0;
selectOptionsPanel.onscroll = scrollHandle;
scrollHandle();
} else {
scrollBar.style.display = "none";
}
moveFun(select.selectedIndex, true, false);
let offset = select.selectedIndex * optionHeight + (optionHeight - panelHeight) / 2;
if (offset > selectOptionsPanel.scrollHeight - panelHeight) {
offset = selectOptionsPanel.scrollHeight - panelHeight;
}
selectOptionsPanel.scrollTop = offset;
};
let changeSelectValue = function (value) {
select.value = value;
select.dispatchEvent(new Event("change"));
moveFun(select.selectedIndex, true, true);
};
let calcIndex = function (e) {
return Math.floor(optionsCount * (e.clientY - selectOptionsPanel.getClientRects()[0].y + selectOptionsPanel.scrollTop) / selectOptionsPanel.scrollHeight);
};
let keyDownHandle = function (event) {
let e = event || arguments.callee.caller.arguments[0];
if (!e) {
return;
}
let keyCode = e.keyCode;
if (keyCode == 27) {
selectPopupPanel.blur();
return;
}
let lastTime = selectPopupPanel.lastTime || 0;
let nowTime = new Date().getTime();
if (nowTime - lastTime < 80) {
return;
}
selectPopupPanel.lastTime = nowTime;
let index = lastFoucsIndex;
if (keyCode == 38) {
if (index > 0) {
moveFun(--index, false, true);
}
} else if (keyCode == 40) {
if (index + 1 < optionsCount) {
moveFun(++index, false, true);
}
} else if (keyCode == 13 || keyCode == 32) {
changeSelectValue(options[index].value);
selectPopupPanel.blur();
} else {
let minPos = Number.MAX_VALUE;
let indexArr = chars[String.fromCharCode(keyCode)];
if (indexArr) {
if (nextIndex >= indexArr.length || lastKeyCode != keyCode) {
nextIndex = 0;
}
index = indexArr[nextIndex++];
}
moveFun(index, false, true);
}
lastKeyCode = keyCode;
};
let reset = function () {
moveFun(lastSelectedIndex, false, false);
lastKeyCode = 0;
nextIndex = 0;
lastFoucsIndex = -1;
}
selectOptionsPanel.onclick = function (e) {
let index = calcIndex(e);
if (index >= optionsCount) {
index = optionsCount - 1;
}
changeSelectValue(options[index].value);
selectPopupPanel.blur();
};
selectPopupPanel.onblur = function () {
hideSelectPanel();
};
selectOptionsPanel.onmousemove = function (e) {
let index = calcIndex(e);
if (index < optionsCount && index != lastFoucsIndex) {
moveFun(index, false);
}
};
selectPopupPanel.onkeydown = function (event) {
keyDownHandle(event);
};
selectPopupPanel.onkeyup = function (event) {
selectPopupPanel.lastTime = new Date().getTime;
};
return {calcRect: calcRect, changeSelectValue: changeSelectValue, move: moveFun, reset: reset};
}
}
function restore(value) {
let select = null;
if (typeof value == "string") {
select = document.getElementById(value);
} else {
select = value;
}
let options = select.options;
let optionsCount = options.length;
for (let i = 0; i < optionsCount; i++) {
options[i].style.display = "block";
}
if (select) {
let selectPanelPosition = select.parentNode.getElementsByClassName("selectPanelPosition")[0];
if (selectPanelPosition) {
selectPanelPosition.remove();
}
if (select.handles) {
Object.keys(select.handles).forEach(function (key) {
select.removeEventListener(key, select.handles[key]);
});
}
}
}
var s = document.getElementById("s1");
initSelect(s);
s.onchange = function (e) {
showInfo("select:" + e.target.value);
}
c1.onchange = function () {
if (c1.checked) {
initSelect("s1");
} else {
restore("s1");
}
}
function showInfo(text) {
if (!info.textContent.endsWith(":")) {
info.textContent += ", ";
}
info.textContent += text;
if (info.scrollHeight > 300) {
info.scrollTop = info.scrollHeight - 300;
}
}
info.ondblclick = function () {
info.textContent = "info:"
};
</script>
</body>
</html>