IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    Lynan发表于 2024-09-26 13:33:59
    love 0

      封面来自 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 元素来实现我的需求,因为它的属性简单清晰。

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

    所以我用自定义 <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();
    }
    }
    })
    })();


沪ICP备19023445号-2号
友情链接