Your Own Select Widget to Boost Productivty

Your Own Select Widget to Boost Productivty

本文同时提供阅读于:简体中文

   Cover Image by Mark Eder

DEMO


Here is the DEMO, try click the inputs on below:







The Making

Step 1. Show a Dropdown Menu When Clicking Input

My first thought was to add a click Eventlistener to each input element, but then I realized that when the document is loaded, it doesn’t mean that the page won’t add new input elements as the content is updated.

So to solve this problem, we can add a click event listener to the body. When the click event emitted by the Input bubbles up, we can do what we need to do - display the Dropdown menu.

The code is like this:

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();
}
});

Step 2. Append Dropdown Menu to the Bottom of Current Input Element

We could use getBoundingClientRect() to get the current Input elements offset positions (relative to it’s parent element).

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 });
}
});

Step 3. Hide Dropdown Menu

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();
}
}
});

Why not

Use HTML <select> Element?

In fact, I did initially use the Select element to achieve my needs because of its simple and clear properties.

However, because its properties are too simple (only label and value), when I need to display tags content, they have nowhere to go.

So I rewrote the dropdown element using custom <div> and styles.

Full Code

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();
}
}
})
})();

Your Own Select Widget to Boost Productivty

https://thelynan.com/easy-fill/

Author

Lynan

Posted on

2024-09-26

Licensed under

Comments