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
让我们继续切入正题
思路实现
经过我许久的思考,想处如下方法实现
-
定义3个全局变量:分别是
变量名 类型 作用 currentMenu object 当前显示的级别最小的菜单
inMenu bool 鼠标指针是否在菜单上
menuIndex number 当前选中菜单选项在菜单列表的编号
- 定义三个类:MenuL,MenuO,Menu(menu-c)
- MenuL主要方法及属性
名 类型 作用 items array 存储MenuO的序列 show function 显示菜单 hide function 隐藏菜单 - MenuO主要方法及属性
名 类型 作用 type
boolean 选项是普通选项还是菜单展开器 index number 菜单选项在菜单列表的编号 keyhover function 获取键盘焦点触发的事件 hover function 鼠标移动到此元素上方时触发的事件 - Menu主要方法及属性
名 类型 作用 show function 显示 hide function 隐藏 -
函数:deleter(menu)用递归方式让menu及其子菜单隐藏
-
open属性用于menu-l,标记已打开的菜单
-
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),并将菜单绑定在按钮上,实现更完美的功能。
原创不易,并且实现菜单功能,我想了很久,遇到过许多废掉的半成品,过程很艰难,那来个三连吧,鼓励作者一下下~