HTML5自定义元素:菜单的实现

Web 组件的一个关键特性是创建自定义元素:即由 Web 开发人员定义行为的 HTML 元素,扩展了浏览器中可用的元素集。

所以我们可以用自定义元素创造属于我们自己的多样化的元素

元素概念

  • menu-l即menu list(菜单列表),用于向用户展示菜单选项列表
  • menu-o即menu option(菜单选项),用于向用户展示单个的菜单选项
  • menu-c即menu content(菜单内容),用于表示一整个菜单元素。由菜单头,菜单列表组成
  • main-menu-l即main menu list(主菜单列表),menu-l的一种,直接显示菜单列表而不是通过展开方式显示

效果

小彩蛋

语法糖

在我们coding的过程中,会遇到重复的代码。比如

  • document.querySelector("")
  • document.querySelectorAll("")
  • HTMLElement.setAttribute(key,value)

    HTMLElement.removeAttribute(key)
  • HTMLElement.addEventListener(event,handle)
  • HTMLElement.hasAttribute(key)
  • document.createElement(element)
  • HTMLElement.appendChild(element)

而这些语句长度也比较大,我写了一个dom.js,简化代码

const doc=document,win=window;
function $(v,p=doc){
    return p.querySelector(v);
}
function all(v,p){
    return p.querySelectorAll(v);
}
function get(k,p){
    return p.getAttribute(k);
}
function set(k,v,p){
    return p.setAttribute(k,v);
}
function ele(n,l={}){
    const e=doc.createElement(n);
    for(const i in l){
        e[i]=l[i];
    }
    return e;
}
function add(e,p){
    p.appendChild(e);
}
function addele(n,l={},p){
    const e=ele(n,l);
    add(e,p);
}
function listen(n,f,p){
    return p.addEventListener(n,f);
}
function has(a,e){
    return e.hasAttribute(a);
}
function rem(a,e){
    if(e)return e.removeAttribute(a);
}
function isClass(element,class_){
    return element.tagName===class_;
}

css属性

css中,我们时常要用color:black;background:white等等"固定"的属性,但你有想过万一用户不喜欢这种配色方案而切换?万一用户希望如background:black;color:white的样式呢?这需要手动修改所有的属性吗?

不用!css自定义属性能帮助你解决这个问题。

向上面的问题,我们只需定义如下的css属性,它很像大多数编程语言的变量,存储一个数据

:root{
    --padding:0.25em;
    --radius:0.5em;
    --border:0.03125em;

    --fg: #000;
    --bg:#fff;
    --color:#0af;
    --color7-5:color-mix(in srgb,var(--color)7.5%,transparent);
    --color15:color-mix(in srgb,var(--color)15%,transparent);
    --color25:color-mix(in srgb,var(--color)25%,transparent);
    --color30:color-mix(in srgb,var(--color)30%,transparent);
    --color60:color-mix(in srgb,var(--color)60%,transparent);
}

▲root.css

让我们继续切入正题

思路实现

经过我许久的思考,想处如下方法实现

  1. 定义3个全局变量:分别是

    变量名类型作用
    currentMenuobject

    当前显示的级别最小的菜单

    inMenubool

    鼠标指针是否在菜单上

    menuIndexnumber

    当前选中菜单选项在菜单列表的编号

  2. 定义三个类:MenuL,MenuO,Menu(menu-c)
  3. MenuL主要方法及属性
    类型作用
    itemsarray存储MenuO的序列
    showfunction显示菜单
    hidefunction隐藏菜单
  4. MenuO主要方法及属性
    类型作用

    type

    boolean选项是普通选项还是菜单展开器
    indexnumber菜单选项在菜单列表的编号
    keyhoverfunction获取键盘焦点触发的事件
    hoverfunction鼠标移动到此元素上方时触发的事件
  5. Menu主要方法及属性
    类型作用
    showfunction显示
    hidefunction隐藏
  6. 函数:deleter(menu)用递归方式让menu及其子菜单隐藏

  7. open属性用于menu-l,标记已打开的菜单

  8. current属性用于menu-o,标记当前选中的选项

通过上述思路,就可以Coding出来了

Coding

menu.css

nav{
    padding-top: var(--padding);
}
/*menu V1.0*/
menu-c{
    user-select: none;/*用户不能选择文本,避免因选择文本出发意想不到的错误*/
    white-space: nowrap;/*不换行*/
}
menu-o{
    padding: var(--padding);
    border-radius: var(--radius);
    /*这样看起来比较圆润*/
    display: inline-flex;
    flex-direction: row;
    align-items: center;
    gap:0.25em;
    /*让图标与文字对齐*/
    box-sizing: border-box;
    width: 100%;
}
menu-l{
    background: var(--bg);
    border: var(--border) solid var(--color);
    padding: var(--padding);
    border-radius: var(--radius);

    position: absolute;/*菜单列表浮动*/
    transform: translateY(calc(0px - var(--padding) - var(--border)));
    /*让这个菜单与上一级菜单持平*/
}
menu-c>menu-l{
    display: none;
    /*一开始是不显示它的*/
}
menu-l[open]/*菜单列表的open属性是显示的标志*/{
    display: inline-grid;
}
menu-o>img/*让图标大小与文字一致*/{
    height: 1.25em;
}
menu-o[current]/*菜单选项的current属性是菜单选中的标志*/{
    background: var(--color30);
}
menu-c>menu-o::after/*后继菜单的箭头标志,用Arial字体才能正常显示*/{
    content: '►';
    font-family: Arial;
}
main-menu-l{
    display: grid;
}

menu.js


var currentMenu=null,//当前显示的级别最小的菜单
    inMenu=false,//指针是否在菜单上
    menuIndex=-1;//当前选中菜单选项在菜单列表的编号
function deleter(l=new MenuL()){//递归实现隐藏菜单及其子菜单
    for(const i of l.items){
        if(i.type===MENUHEAD_OPT){
            i.menuc.hide();
            deleter(i.menuc.mlist);
        }
    }
}
class MenuL extends HTMLElement{
    constructor() {
        super();
    }
    connectedCallback() {
        this.items=[];//选项序列
        this.parentl=this.parentElement.parentElement;//父菜单列表
    }
    show(){
        menuIndex=0;
        this.open();
        console.log(this.items)
        if(this.items.length===0) {
            let c = 0;
            for (const i of this.childNodes) {
                if (isClass(i, "MENU-O")) {
                    i.index = c++;
                    this.items.push(i);
                }
                if (isClass(i, "MENU-C")) {
                    i.hdopt.index = c++;
                    this.items.push(i.hdopt);
                }
            }
        }
        this.items[menuIndex].keyhover();//默认第一个
        if(this.parentl.tagName==="MENU-L"){
            this.parentl.current=false;
        }
    }
    hide(){
        //console.log(this,"hide");
        this.close();
    }
    open(){
        set("open",'',this);
    }
    close(){
        rem("open",this);
        deleter(this);
    }
}
NORMAL_OPT=0
MENUHEAD_OPT=1
class MenuO extends HTMLElement{
    constructor() {
        super();
    }
    connectedCallback(){
        this.type=(isClass(this.parentElement,"MENU-L")?NORMAL_OPT:MENUHEAD_OPT);
        if(this.type===MENUHEAD_OPT)this.menuc=this.parentElement;//所在的菜单
        this.index=-1;//编号,用于填充menuIndex
        listen("mouseenter",this.hover,this);
    }
    enter(){
        this.setAttribute("current",'');
    }
    leave(){
        this.removeAttribute("current")
    }
    keyhover(){//键盘获取焦点(此焦点非彼焦点)
        this.enter()
        let k=this.parent();
        if(!k.items)return;
        for (const i of k.items) {
                if (i !== this){
                    i.leave();
            }
        }
        this.enter();
        return k;
    }
    hover(){
        this.keyhover();
        menuIndex=this.index;
        let k=this.parent();
        //console.log(k);
        if(!k.items)return;
        for(const i of k.items){
            if(i!==this&&i.type===MENUHEAD_OPT){
                i.menuc.hide();
            }
        }
        if(this.type===MENUHEAD_OPT){
            this.menuc.show();
            //console.log("open menu ",this.menuc);
            currentMenu=this.menuc.mlist;

            //console.log(currentMenu);
        }
        k.index=this.index;
    }
    parent(){
        let k;
        if(this.type===NORMAL_OPT) {
            k=this.parentElement;
        }else if(this.type===MENUHEAD_OPT){
            k=this.parentElement.parentElement;
        }
        return k;
    }

}
class Menu extends HTMLElement{
    constructor() {
        super();
    }
    connectedCallback(){
        this.hdopt=this.querySelector("menu-o");
        this.mlist=this.querySelector("menu-l");

        listen("mouseleave",this.leave,this);
        listen("mousemove",this.move,this)
    }
    show(){
        if(this.mlist){
            //console.log(this.mlist)
            this.mlist.show();
            //console.log("set CURRENT",this.mlist);
            currentMenu=this.mlist;
        }
    }
    hide(){
        if(this.mlist){
            this.mlist.hide();
            currentMenu=this.mlist.parentl;
        }
    }
    move(){
        inMenu=true;
    }
    leave(){
        inMenu=false;
    }
}
class MainMenuL extends HTMLElement{
    constructor() {
        super();
    }
    connectedCallback() {
        this.setAttribute("tabindex",'0')
        this.visible=false;//看得见
        this.current=false;//叶子节点
        this.optindex=-1;//当前选中菜单编号
        this.items=[];
        this.addEventListener("keydown",this.key);
        this.parentl=this;
        this.show();
        this.addEventListener("keydown",(e)=>{
            if(currentMenu){
                const n=currentMenu.items.length,i=menuIndex;
                //console.log(menuIndex)
                switch (e.code){
                    case "ArrowUp":
                        menuIndex=(i-1+n)%n;
                        currentMenu.items[menuIndex].keyhover();
                        console.log("Up arrow");
                        break;
                    case "ArrowDown":
                        menuIndex=(i+1)%n;
                        currentMenu.items[menuIndex].keyhover();
                        console.log("Down arrow");
                        break;
                    case "ArrowLeft":
                        currentMenu.hide();
                        currentMenu=currentMenu.parentl;
                        console.log("Left arrow");
                        break;
                    case "ArrowRight":
                        currentMenu.items[menuIndex].hover();
                        console.log("Right arrow");
                        break;
                    case "Enter":
                        currentMenu.items[menuIndex].click();
                        break;
                }
            }
        });
    }
    show(){
        this.visible=this.current=true;
        menuIndex=0;
        this.open();
        this.items=[];
        let c=0;
        for(const i of this.childNodes){
            if(i.tagName==="MENU-O"){
                i.index=c++;
                this.items.push(i);
            }if(i.tagName==="MENU-C"){
                i.hdopt.index=-1;
                this.items.push(i.hdopt);
            }
        }
        this.items[menuIndex].keyhover();
        const k=this.parentElement.parentElement
        if(k.tagName==="MENU-L"){
            k.current=false;
        }
    }
    hide(){
        //console.log("hide");
        this.visible=this.current=false;
        this.close();
    }
    open(){
        this.setAttribute("open",'');
    }
    close(){
        this.removeAttribute("open");
    }
}
window.customElements.define("menu-l",MenuL);
window.customElements.define("menu-o",MenuO);
window.customElements.define("menu-c",Menu);

window.customElements.define("main-menu-l",MainMenuL);
window.addEventListener("click",()=>{
    if(!inMenu){
        for(const i of doc.querySelectorAll("menu-l")){
            i.hide();
        }
    }
})

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
    <link rel="stylesheet" href="root.css">
    <script src="script.js"></script>



    <link rel="stylesheet" href="act-option.css">
    <link rel="stylesheet" href="app-layout.css">
    <link rel="stylesheet" href="button-area.css">
    <link rel="stylesheet" href="m-button.css">
    <link rel="stylesheet" href="theme.css">
    <link rel="stylesheet" href="h5element.css">
    <link rel="stylesheet" href="m-ct.css">
    <script defer src="unruled-button-area.js"></script>
    <script defer src="button-area.js"></script>
    <script defer src="jquery.js"></script>
    <script defer src="m-button.js"></script>
    <link rel="stylesheet" href="menu.css">

    <script defer src="menu.js"></script>
    <link rel="stylesheet" href="menu-o.css">
</head>
<body>
<header>

</header>
<nav>
    <main-menu-l>
        <menu-c>
            <menu-o>文件</menu-o>
            <menu-l>
                <menu-c>
                    <menu-o>新建</menu-o>
                    <menu-l>
                        <menu-c>
                            <menu-o>HTML文件</menu-o>
                            <menu-l>
                                <menu-o>HTML5</menu-o>
                                <menu-o>HTML4</menu-o>
                            </menu-l>
                        </menu-c>
                        <menu-o>CSS层叠样式表</menu-o>
                        <menu-o>JavaScript脚本</menu-o>
                    </menu-l>
                </menu-c>
                <menu-o>打开</menu-o>
                <menu-c>
                    <menu-o>保存</menu-o>
                    <menu-l>
                        <menu-o>覆盖原文件</menu-o>
                        <menu-o>另存为</menu-o>
                    </menu-l>
                </menu-c>
            </menu-l>
        </menu-c>
        <menu-c>
            <menu-o>编辑</menu-o>
            <menu-l>
                <menu-o>撤销</menu-o>
                <menu-o>恢复</menu-o>
                <menu-o>剪切</menu-o>
                <menu-o>复制</menu-o>
                <menu-o>粘贴</menu-o>
            </menu-l>
        </menu-c>
    </main-menu-l>
</nav>
<section>
</section>
<footer>
    Copyright
</footer>
</body>
</html>

结尾

MainMenuL这个元素并不是我最初设计的初衷,只是后来方便实现显示菜单的功能,在菜单外嵌套一个菜单列表元素。下期我们将实现MButton(多功能按钮Mutifunction Button),并将菜单绑定在按钮上,实现更完美的功能。

原创不易,并且实现菜单功能,我想了很久,遇到过许多废掉的半成品,过程很艰难,那来个三连吧,鼓励作者一下下~

  • 9
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值