创建一个自定义的下拉菜单

创建一个自定义的下拉菜单

This article is also available in: English.

   封面来自 Mark Eder

DEMO


下方是 Demo 展示区域,点击即可输入框触发下拉菜单。







过程

步骤 1. 点击输入框时显示下拉菜单

我最初的想法是给每个输入元素添加一个点击事件监听器,但后来我意识到,当文档加载完成时,并不意味着页面不会随着内容的更新而添加新的输入元素。

因此,为了解决这个问题,我们可以给 body 添加一个点击事件监听器。当 Input 发出的点击事件冒泡时,我们就可以做我们需要做的事情——显示下拉菜单。

(伪)代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
const showSelect = () =>{
// create a dropdown element and append to document
// ...
}

document.addEventListener("click", (e) => {
const target = e.target;

if (target.tagName === "INPUT" && ["text", "password"].includes(target.type)) {
showSelect();
}
});

步骤 2. 将下拉菜单附加到当前输入元素的底部

我们可以使用“getBoundingClientRect()”来获取当前输入元素的偏移位置(相对于其父元素)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let currentInput = null
const showSelect = ({top, left, width}) =>{
// create a dropdown element and append to document
const dropdownElement = document.createElement("div");
// ...
const inputParentElement = currentInput.parentElement;
if(!["fixed", "absolute"].includes(inputParentElement.position)){
inputParentElement.style.position = 'relative';
const {offsetTop, offsetLeft, offsetHeight} = currentInput
dropdownElement.style.position = 'absolute'
dropdownElement.style.top = offsetTop + offsetHeight + 'px'
dropdownElement.style.left = offsetLeft + 'px'
inputParentElement.appendChild(dropdownElement)
}else{
document.body.appendChild(dropdownElement);
}
}

document.addEventListener("click", (e) => {
const target = e.target;

if (target.tagName === "INPUT" && ["text", "password"].includes(target.type)) {
currentInput = target;
const { x, y, width, height } = currentInput.getBoundingClientRect();
showSelect({ top: y + height, left: x, width });
}
});

步骤3.隐藏下拉菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const hideSelect = () => {
const selectwrapElement = document.querySelector("#selectwrapElement");
if (selectwrapElement) selectwrapElement.parentElement.removeChild(selectwrapElement);
};

// hide on clicking an option in dropdown menu
option.onclick = () => {
currentInput.value = value;
hideSelect();
};

// hide on press `Escape` or input anything in current input
document.addEventListener("keyup", (e) => {
if (document.querySelector("#selectwrapElement")) {
if (
e.code === "Escape" ||
document.activeElement.tagName === "INPUT"
) {
hideSelect();
}
}
});

为什么不

使用 <select> 元素?

其实我一开始确实用 Select 元素来实现我的需求,因为它的属性简单清晰。

但是因为它的属性太简单(只有 labelvalue),当我需要显示标签内容时,它们无处可去。

所以我用自定义 <div> 和样式重写了下拉元素。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
(function(){
let currentInput = null;
const targetsStack = [];
const options = [
// "A123",
{
label: "Test Account",
value: "A223",
tags: ["Tag1", "Tag2"],
},
{
label: "Test Password",
value: "123456789",
type: "password",
},
{
label: "Test ID",
value: "33225552",
},
];
const hideSelect = () => {
const selectwrapElement = document.querySelector("#selectwrapElement");
if (selectwrapElement) selectwrapElement.parentElement.removeChild(selectwrapElement);
};

const showSelect = (position, currentValue) => {
const wrapElement = document.createElement("div");
const styles = {
wrapElement: `position: fixed; top: ${position.top}px; left: ${position.left}px; width: ${position.width}px; z-index: 999; font-family: system-ui; background-color: #fff; box-shadow: rgba(0, 0, 0, 0.02) 0px 1px 1px, rgba(0, 0, 0, 0.02) 0px 2px 2px, rgba(0, 0, 0, 0.02) 0px 4px 4px, rgba(0, 0, 0, 0.02) 0px 8px 8px, rgba(0, 0, 0, 0.02) 0px 16px 16px; max-height: 50vh;`,
selectElement: `width: 100%; overflow: auto;`,
option:
"box-sizing: border-box; padding: 5px; border-bottom: 0.5px solid #c3c3c3; cursor: pointer;",
optionLabel: "font-weight: bold; font-size: 14px;",
optionValue: "font-size: 12px; color: #232323;",
tagWrap: "display: flex; flex-wrap: wrap;",
tagItem:
"background-color: #003333; padding: 2px 5px; margin-top: 3px;margin-right: 3px; color: white; border-radius: 1px; font-size: 10px;",
selectedBackgroundCOlor: "rgba(100, 108, 255, 0.14)",
};
wrapElement.id = "selectwrapElement";
wrapElement.style = styles.wrapElement;
const selectElement = document.createElement("div");
selectElement.style = styles.selectElement;
selectElement.size = options.length;
options.forEach((item, index) => {
const option = document.createElement("div");
option.style = styles.option;
if (index === options.length - 1) {
option.style.borderBottom = "none";
}
const label = typeof item === "string" ? item : item.label;
const value = typeof item === "string" ? item : item.value;
let displayedValue = value;
if (typeof item === "object" && item.type === "password") {
displayedValue = value.replace(/./g, "*");
}
const optionLabel = document.createElement("div");
optionLabel.innerText = label;
optionLabel.style = styles.optionLabel;
const optionValue = document.createElement("span");
optionValue.innerText = displayedValue;
optionValue.style = styles.optionValue;
const slelected = value === currentValue;
if (slelected) {
option.style.backgroundColor = styles.selectedBackgroundCOlor;
}
option.appendChild(optionLabel);
if (typeof item === "object" && item.tags) {
const tagWrap = document.createElement("div");
tagWrap.style = styles.tagWrap;
item.tags.forEach((tag) => {
const tagItem = document.createElement("span");
tagItem.innerText = tag;
tagItem.style = styles.tagItem;
tagWrap.appendChild(tagItem);
});
option.appendChild(tagWrap);
}
option.appendChild(optionValue);
option.onclick = () => {
currentInput.value = value;
hideSelect();
};
selectElement.appendChild(option);
});
wrapElement.appendChild(selectElement);
const inputParentElement = currentInput.parentElement;
if(!["fixed", "absolute"].includes(inputParentElement.position)){
inputParentElement.style.position = 'relative';
const {offsetTop, offsetLeft, offsetHeight} = currentInput
wrapElement.style.position = 'absolute'
wrapElement.style.top = offsetTop + offsetHeight + 'px'
wrapElement.style.left = offsetLeft + 'px'
inputParentElement.appendChild(wrapElement)
}else{
document.body.appendChild(wrapElement);
}
};

document.addEventListener("click", (e) => {
const target = e.target;
if (targetsStack.length === 2) {
targetsStack.shift();
}
targetsStack.push(target);

if (!targetsStack.includes(currentInput)) {
hideSelect();
}

if (
target.tagName === "INPUT" &&
["text", "password"].includes(target.type)
) {
currentInput = target;
const { x, y, width, height } = currentInput.getBoundingClientRect();
if (document.querySelector("#selectwrapElement")) hideSelect();
showSelect(
{ top: y + height, left: x, width: width },
currentInput.value
);
}
});

document.addEventListener("keyup", (e) => {
if (document.querySelector("#selectwrapElement")) {
if (
e.code === "Escape" ||
document.activeElement.tagName === "INPUT"
) {
hideSelect();
}
}
})
})();

创建一个自定义的下拉菜单

https://lynan.cn/easy-fill/

作者

Lynan

发布于

2024-09-26

许可协议

评论