JS案例:在浏览器实现自定义菜单

news2024/10/7 14:33:24

目录

前言

设计思路

BaseElem

CustomElement

BaseDrag

Drag

Resize

最终效果

总结

相关代码


前言

分享一下之前公司实现自定义菜单的思路,禁用浏览器右键菜单,使用自定义的菜单将其代替,主要功能有:鼠标右键调出菜单,双击选中/取消选中标签,新建标签,删除标签,调整位置,调整大小,取消拖拽,关闭菜单

设计思路

  • MessageCenter来自于消息中心,为组件提供基础通信功能
  • BaseElem: 自定义标签的基类,提供一些通用的方法和属性,继承自MessageCenter,通过消息中心对外通信
  • Menu: 菜单类,用于创建和显示自定义菜单。它继承自BaseElem,实现创建菜单、渲染菜单列表等方法
  • CustomElement: 自定义元素类,用于创建和操作自定义标签。它继承自BaseElem,提供创建标签、选中标签、复制标签、删除标签等方法
  • BaseDrag: 拖拽基类,提供了基本的拖拽功能。它继承自BaseElem,实现鼠标事件的处理和触发
  • Drag: 拖拽调整标签位置类,继承自BaseDrag,实现拖拽标签位置的功能
  • Resize: 拖拽调整标签尺寸类,继承自BaseDrag,实现拖拽调整标签尺寸的功能。

BaseElem

自定义标签基类提供了移动和删除标签功能,它充当公共类的作用,后面的自定义标签都继承与该类

/**
 * 自定义标签的基类
 */
class BaseElem extends MessageCenter {
    root: HTMLElement = document.body
    remove(ele: IParentElem) {
        ele?.parentNode?.removeChild(ele)
    }
    moveTo({ x, y }: { x?: number, y?: number }, ele: IParentElem) {
        if (!ele) return
        ele.style.left = `${x}px`
        ele.style.top = `${y}px`
    }
}

Menu

菜单类的作用是创建自定义菜单,代替浏览器原有的右键菜单。其中每个菜单子项的数据结构如下

type MenuListItem = {
    label: string
    name?: string
    handler?(e: MouseEvent): void
}

菜单类

export class Menu extends BaseElem {
    constructor(public menuList: MenuListItem[] = [], public menu?: HTMLElement) {
        super()
        this.root.addEventListener("contextmenu", this.menuHandler)
    }
    /**
     * 创建菜单函数
     * @param e 
     */
    menuHandler = (e: MouseEvent) => {
        e.preventDefault();// 取消默认事件
        this.remove(this.menu)
        this.create(this.root)
        this.moveTo({
            x: e.clientX,
            y: e.clientY
        }, this.menu)
        this.renderMenuList()
    }
    /**
     * 创建菜单元素
     * @param parent 父元素
     */
    create(parent: HTMLElement) {
        this.menu = createElement({
            ele: "ul",
            attr: { id: "menu" },
            parent
        })
    }
    /**
     * 菜单列表
     * @param list 列表数据
     * @param parent 父元素
     * @returns 
     */
    renderMenuList(list: MenuListItem[] = this.menuList, parent: IParentElem = this.menu) {
        if (!parent) return
        list.forEach(it => this.renderMenuListItem(it, parent))
    }
    /**
     * 菜单列表子项
     * @param item 单个列表数据
     * @param parent 父元素
     * @returns 列表子项
     */
    renderMenuListItem(item: MenuListItem, parent: HTMLElement) {
        const li = createElement({
            ele: "li",
            attr: {
                textContent: item.label
            },
            parent
        })
        li.addEventListener("click", item.handler ?? noop)
        return item
    }
}

我们在HTML中使用一下菜单功能,通过label配置菜单选项,handler设置点击事件

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Menu</title>
    <style>
        html,
        body {
            width: 100%;
            height: 100%;
        }

        #menu {
            z-index: 2;
            position: fixed;
            width: 100px;
            min-height: 40px;
            background: lightcoral;
        }

        #menu li {
            text-align: center;
            line-height: 30px;
            cursor: pointer;
        }

        #menu li:hover {
            background: lightblue;
        }
    </style>
</head>

<body>
    <script type="module">
        import { Menu } from "./index.js"
        // 初始化菜单功能
        const menu = new Menu([

            {
                label: "关闭", handler: (e) => {
                    menu.remove(menu.menu)
                }
            }
        ])

    </script>
</body>

</html>

效果如下 

CustomElement

为了让菜单与被控标签解耦(实际上也没有联系),使用新的类承载标签管理。其中自定义标签主要包含以下功能:

create:新建标签

cloneNode:复制标签

removeEle:删除标签

select:选中/取消选中标签(通过双击触发该函数)

setCount:标签的计数器

export class CustomElement extends BaseElem {
    selectClass = "custom-box"// 未被选中标签class值
    private _selectEle: ICustomElementItem = null// 当前选中的标签
    count: number = 0// 计数器,区分标签
    constructor() {
        super()
        document.onselectstart = () => false// 取消文字选中
    }
    /**
     * 选中标签后的样式变化
     */
    set selectEle(val: ICustomElementItem) {
        const { _selectEle } = this
        this.resetEleClass()
        if (val && val !== _selectEle) {
            val.className = `select ${this.selectClass}`
            this._selectEle = val
        }
    }
    get selectEle() {
        return this._selectEle
    }
    /**
     * 初始化事件
     * @param ele 
     */
    initEve = (ele: HTMLElement) => {
        ele.addEventListener("dblclick", this.select)
    }
    /**
     * 复制标签时增加复制文本标识
     * @param elem 
     */
    setCount(elem: HTMLElement) {
        elem.textContent += "(copy)"
        ++this.count
    }
    /**
     * 选中标签后重置上一个标签的样式
     * @returns 
     */
    resetEleClass() {
        if (!this._selectEle) return
        this._selectEle.className = this.selectClass
        this._selectEle = null
    }
    /**
     * 新建标签
     * @returns 标签对象
     */
    create() {
        const ele = createElement({
            ele: "div",
            attr: { className: this.selectClass, textContent: (++this.count).toString() },
            parent: this.root
        })
        return ele
    }
    /**
     * 初始化标签
     * @param e 鼠标事件
     * @param elem 标签对象
     */
    add(e: MouseEvent, elem?: HTMLElement) {
        const ele = elem ?? this.create()
        ele && this.initEve(ele)
        this.moveTo({
            x: e.clientX,
            y: e.clientY
        }, ele)
    }
    /**
     * 复制标签操作
     * @param e 鼠标事件
     * @returns 
     */
    cloneNode(e: MouseEvent) {
        if (!this.selectEle) return
        const _elem = this.selectEle?.cloneNode?.(true) as HTMLElement
        _elem && this.root.appendChild(_elem)
        _elem && this.setCount(_elem)
        this.add(e, _elem)
        this.selectEle = _elem
    }
    /**
     * 删除标签
     * @returns 
     */
    removeEle() {
        if (!this.selectEle) return
        this.remove(this.selectEle as IParentElem)
        this.selectEle = null
        --this.count
    }
    /**
     * 选中/取消选中标签
     * @param e 
     */
    select = (e: MouseEvent) => {
        this.selectEle = e.target
    }
    /**
     * 点击body时取消选中(未使用)
     * @param e 
     */
    unselected = (e: MouseEvent) => {
        if (e.target === this.root) this.selectEle = null
    }
}

结合上述类的实现,我们在页面中增加几种菜单

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Menu</title>
    <style>
        html,
        body {
            width: 100%;
            height: 100%;
        }

        #menu {
            z-index: 2;
            position: fixed;
            width: 100px;
            min-height: 40px;
            background: lightcoral;
        }

        #menu li {
            text-align: center;
            line-height: 30px;
            cursor: pointer;
        }

        #menu li:hover {
            background: lightblue;
        }

        .custom-box {
            line-height: 100px;
            text-align: center;
            width: 100px;
            height: 100px;
            white-space: nowrap;
            text-overflow: ellipsis;
            overflow: hidden;
            background: lightgreen;
            position: fixed;
            cursor: move;
        }

        .select {
            z-index: 1;
            border: 3px solid black;
        }
    </style>
</head>

<body>
    <script type="module">
        import { Menu, CustomElement } from "./index.js"
        // 初始化标签
        const elem = new CustomElement()
        // 初始化菜单功能
        const menu = new Menu([
            {
                label: "新建", handler: (e) => {
                    menu.remove(menu.menu)
                    elem.add(e)
                }
            }, {
                label: "复制", handler: (e) => {
                    menu.remove(menu.menu)
                    elem.cloneNode(e)
                }
            }, {
                label: "删除", handler: (e) => {
                    menu.remove(menu.menu)
                    elem.removeEle()
                }
            },
            {
                label: "关闭", handler: (e) => {
                    menu.remove(menu.menu)
                }
            }
        ])

    </script>
</body>

</html>

效果如下

BaseDrag

完成上述基础功能后,我们可以尝试对标签位置和大小进行修改,所以我们建立一个鼠标拖拽的基类,用来实现拖拽的公共函数

/**
 * 拖拽基类
 */
class BaseDrag extends BaseElem {
    constructor(public elem: HTMLElement, public root: any = document) {
        super()
        this.init()
    }
    /**
     * 初始化事件
     */
    init() {
        this.elem.onmousedown = this.__mouseHandler//添加点击事件,避免重复定义
    }
    /**
     * 将一些公共函数在基类中实现
     * @param e 事件对象
     */
    private __mouseHandler = (e: Partial<MouseEvent>) => {
        const { type } = e
        if (type === "mousedown") {
            this.root.addEventListener("mouseup", this.__mouseHandler);
            this.root.addEventListener("mousemove", this.__mouseHandler);
        } else if (type === "mouseup") {
            this.root.removeEventListener("mouseup", this.__mouseHandler);
            this.root.removeEventListener("mousemove", this.__mouseHandler);
        }
        type && this.emit(type, e)// 触发子类的函数,进行后续操作
    }
    /**
     * 取消拖拽
     */
    reset() {
        this.elem.onmousedown = null
    }
}

可以看到,上述的代码的__mouseHandler函数中我们对鼠标事件进行了拦截,并且借助消息中心将事件传递出去,方便后续的拓展

Drag

接着是拖拽移动标签的功能,该类拖拽了鼠标按下和移动的回调

/**
 * 拖拽调整标签位置
 */
export class Drag extends BaseDrag {
    offset?: Partial<{ x: number, y: number }>// 鼠标点击时在元素上的位置
    constructor(public elem: HTMLElement) {
        super(elem)
        this.on("mousedown", this.mouseHandler)
        this.on("mousemove", this.mouseHandler)
    }
    /**
     * 鼠标事件处理函数,当鼠标按下时,记录鼠标点击时在元素上的位置;当鼠标移动时,根据鼠标位置的变化计算新的位置,并通过调用父类的moveTo方法来移动元素
     * @param e 
     */
    mouseHandler = (e: Partial<MouseEvent>) => {
        const { type, target, clientX = 0, clientY = 0 } = e
        if (type === "mousedown") {
            this.offset = {
                x: e.offsetX,
                y: e.offsetY
            }
        } else if (type === "mousemove") {
            const { x = 0, y = 0 } = this.offset ?? {}
            this.moveTo({
                x: clientX - x,
                y: clientY - y
            }, target as HTMLElement)
        }
    }
}

Resize

最后我们将位置改成高度宽度,实现一下调整标签尺寸的类

/**
 * 拖拽调整标签尺寸
 */
export class Resize extends BaseDrag {
    startX?: number
    startY?: number
    startWidth?: IStyleItem
    startHeight?: IStyleItem
    constructor(public elem: HTMLElement) {
        super(elem)
        this.on("mousedown", this.mouseHandler)
        this.on("mousemove", this.mouseHandler)
    }
    /**
     * 获取标签样式项
     * @param ele 标签
     * @param key 样式属性名
     * @returns 样式属性值
     */
    getStyle(ele: Element, key: keyof CSSStyleDeclaration) {
        const styles = document.defaultView?.getComputedStyle?.(ele)
        if (styles && typeof styles[key] === "string") return parseInt(styles[key] as string, 10)
    }
    /**
     * 鼠标事件处理函数,用于处理鼠标按下和移动事件。当鼠标按下时,记录起始位置和当前宽度、高度的值。当鼠标移动时,根据鼠标位置的变化计算新的宽度和高度,并更新元素的样式。
     * @param e 
     */
    mouseHandler = (e: Partial<MouseEvent>) => {
        const { type, clientX = 0, clientY = 0 } = e
        if (type === "mousedown") {
            this.startX = clientX;
            this.startY = clientY;
            this.startWidth = this.getStyle(this.elem, "width")
            this.startHeight = this.getStyle(this.elem, "height")
        } else if (type === "mousemove") {
            const width = <number>this.startWidth + (clientX - <number>this.startX);
            const height = <number>this.startHeight + (clientY - <number>this.startY);
            this.elem.style.width = width + 'px';
            this.elem.style.height = height + 'px';
        }
    }
}

最终效果

最后我们在HTML中使用上述的所有功能,演示一下全部功能

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Menu</title>
    <style>
        html,
        body {
            width: 100%;
            height: 100%;
        }

        #menu {
            z-index: 2;
            position: fixed;
            width: 100px;
            min-height: 40px;
            background: lightcoral;
        }

        #menu li {
            text-align: center;
            line-height: 30px;
            cursor: pointer;
        }

        #menu li:hover {
            background: lightblue;
        }

        .custom-box {
            line-height: 100px;
            text-align: center;
            width: 100px;
            height: 100px;
            white-space: nowrap;
            text-overflow: ellipsis;
            overflow: hidden;
            background: lightgreen;
            position: fixed;
            cursor: move;
        }

        .select {
            z-index: 1;
            border: 3px solid black;
        }
    </style>
</head>

<body>
    <script type="module">
        import { Menu, CustomElement, Drag, Resize } from "./index.js"
        // 初始化标签
        const elem = new CustomElement()
        // 初始化菜单功能
        const menu = new Menu([
            {
                label: "新建", handler: (e) => {
                    menu.remove(menu.menu)
                    elem.add(e)
                }
            }, {
                label: "复制", handler: (e) => {
                    menu.remove(menu.menu)
                    elem.cloneNode(e)
                }
            }, {
                label: "删除", handler: (e) => {
                    menu.remove(menu.menu)
                    elem.removeEle()
                }
            }, {
                label: "调整位置", handler: (e) => {
                    menu.remove(menu.menu)
                    elem.selectEle && (elem.selectEle.__drag = new Drag(elem.selectEle))
                }
            }, {
                label: "调整大小", handler: (e) => {
                    menu.remove(menu.menu)
                    elem.selectEle && (elem.selectEle.__resize = new Resize(elem.selectEle))
                }
            }, {
                label: "取消拖拽", handler: (e) => {
                    menu.remove(menu.menu)
                    elem.selectEle?.__drag?.reset?.()
                    elem.selectEle?.__resize?.reset?.()
                }
            },
            {
                label: "关闭", handler: (e) => {
                    menu.remove(menu.menu)
                }
            }
        ])

    </script>
</body>

</html>

 

总结

当涉及到自定义菜单时,JavaScript提供了丰富的功能和API,让我们能够创建具有定制化选项和交互性的菜单。文章主要介绍了前端自定义菜单的实现过程,描述了创建标签、选中标签、复制标签、删除标签、拖拽位置及大小等功能。

以上就是文章全部内容了,感谢你看到了最后,如果觉得不错的话,请给个三连支持一下吧,谢谢!

相关代码

utils-lib-js: JavaScript工具函数,封装的一些常用的js函数

myCode: 基于js的一些小案例或者项目 - Gitee.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/783811.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

二、基本数据类型和表达式

2.1数据类型 数据类型占用字节数取值范围bool1true 或 falsechar1-128 到 127 或 0 到 255 &#xff08;取决于是否带符号&#xff09;unsigned char10 到 255short2-32,768 到 32,767unsigned short20 到 65,535int4-2,147,483,648 到 2,147,483,647unsigned int40 到 4,294,…

ES6基础知识二:ES6中数组新增了哪些扩展?

一、扩展运算符的应用 ES6通过扩展元素符…&#xff0c;好比 rest 参数的逆运算&#xff0c;将一个数组转为用逗号分隔的参数序列 console.log(...[1, 2, 3]) // 1 2 3console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5[...document.querySelectorAll(div)] // [<div>, &l…

12 扩展Spring MVC

12.1 实现页面跳转功能 页面跳转功能&#xff1a;访问localhost:8081/jiang会自动跳转到另一个页面。 首先&#xff0c;在config包下创建一个名为MyMvcConfig的配置类&#xff1a; 类上加入Configuration注解&#xff0c;类实现WebMvcConfiger接口&#xff0c;实现里面的视图跳…

Python入门【列表元素访问和计数 、切片操作、列表的遍历、复制列表所有的元素到新列表对象、多维列表、元组tuple】(五)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小王&#xff0c;CSDN博客博主,Python小白 &#x1f4d5;系列专栏&#xff1a;python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 &#x1f4e7;如果文章知识点有错误…

OSI 和 TCP/IP 网络分层模型详解(基础)

OSI模型: 即开放式通信系统互联参考模型&#xff08;Open System Interconnection Reference Model&#xff09;&#xff0c;是国际标准化组织&#xff08;ISO&#xff09;提出的一个试图使各种计算机在世界范围内互连为网络的标准框架&#xff0c;简称OSI。 OSI 七层模型 OS…

两个数组的dp问题(1)--动态规划

一)最长公共子序列: 1143. 最长公共子序列 - 力扣&#xff08;LeetCode&#xff09; 一)定义一个状态表示:根据经验题目要求 1)选取第一个字符串[0&#xff0c;i]区间以及第二个字符串[0&#xff0c;j]区间作为研究对象&#xff0c;先选取两段区间研究问题&#xff0c;先求出[0…

即时零售业务调研2022

调研时间22年7月 核心观点&#xff1a; 即时零售业务正处于爆发期疫情催化&#xff0c;线下商家和零售平台处于双向奔赴的蜜月期未来规模会是万亿市场&#xff0c;市场不容小觑&#xff0c;广告业务重要 业务对比优势 平台模式护城河是&#xff1a;线上流量线下配送网络&am…

芯洲科技-降压DCDC开关电源参考选型目录

芯洲科技&#xff0c;是国内领先的中高压DC-DC&#xff08;直流转直流&#xff09;功率转换芯片供应商。北京冠宇铭通 一级代理。 国产化替代&#xff0c;对标TI&#xff0c;有很多料号可直接PIN TO PIN&#xff0c;比如TPS562200(SOT23-6)\TPS563200(SOT23-6)/TPS54540/LMR140…

02、什么是TPS和响应时间

在这个图中&#xff0c;定义了三条曲线、三个区域、两个点以及三个状态描述。 三条曲线&#xff1a;吞吐量的曲线&#xff08;紫色&#xff09;、使用率 / 用户数曲线&#xff08;绿色&#xff09;、响应时间曲线&#xff08;深蓝色&#xff09;。三个区域&#xff1a;轻负载区…

【IDEA】idea 无法打包文件到 target 如何处理.

文章目录 1. 一般原因就是文件的资源没有设置2. 还有可能是打包的配置中没有包含xxx.xml,xxx.sql等3. 还有一种情况是&#xff0c;因为缓存 1. 一般原因就是文件的资源没有设置 比如常见的 maven 项目&#xff0c;必须标记资源的类别&#xff0c;否则就有可能无法编译到target…

unity——Rigidbody(刚体)

官方手册&#xff1a;Rigidbody 官方API&#xff1a;Rigidbody Api 组件简介 刚体 (Rigidbody)使__游戏对象__的行为方式受物理控制。刚体可以接受力和扭矩&#xff0c;使对象以逼真的方式移动。任何游戏对象都必须包含受重力影响的刚体&#xff0c;行为方式基于施加的作用力…

基于机器视觉工具箱和形态学处理的视频中目标形状检测算法matlab仿真

目录 1.算法理论概述 2.部分核心程序 3.算法运行软件版本 4.算法运行效果图预览 5.算法完整程序工程 1.算法理论概述 目标形状检测是计算机视觉领域的重要任务之一&#xff0c;旨在从视频序列中自动检测和识别特定目标的形状。本文介绍一种基于机器视觉工具箱和形态学处理…

Unity自定义后处理——Bloom效果

大家好&#xff0c;我是阿赵。   继续介绍屏幕后处理效果&#xff0c;这一期讲一下Bloom效果。 一、Bloom效果介绍 还是拿这个模型作为背景。 Bloom效果&#xff0c;就是一种全屏泛光的效果&#xff0c;让模型和特效有一种真的在发光的感觉。 根据参数不一样&#xff0c;可…

21 对于对象中set方法的理解

对于一个Student对象&#xff0c;属性有name和age&#xff0c;而age一般是比0大的&#xff0c;赋值不可能是负数&#xff0c;可以通过set方法来控制&#xff01;Student对象如下&#xff1a; public class Student {// 成员变量private int age;public Student() {}public int…

嬴图 | K邻算法在风险传导场景中的实践意义

随着图思维方式与图数据框架在工业领域的有效开展和深入应用&#xff0c;可靠且高效的图算法也就成为了图数据探索、挖掘与应用的基石。该图算法系列&#xff08;具体见推荐阅读&#xff09;&#xff0c;集合了 Ultipa 嬴图团队在算法实践应用中的经验与思考&#xff0c;希望在…

海外NFT玩法入门科普 - Web3.0(二)

咪哥杂谈 本篇阅读时间约为 7 分钟。 1 前言 距离上一篇文章有些时日&#xff0c;把上一次介绍 web3 的海外 nft 的坑继续填完&#xff0c;今天这篇是下篇&#xff0c;其中详细的介绍了我从去年一年来&#xff0c;所见&#xff0c;所参与的玩法&#xff0c;赚钱方式&#xff0c…

VUE开发神器-NVM nodejs版本控制工具nvm

前言 在学习前端框架或者是nodejs时&#xff0c;有时候某些框架对nodejs的版本有要求。但此时你的电脑上已经安装了10.x版本的nodejs&#xff0c;你不想直接更新到12.x&#xff0c;想同时保存10.x和12.x版本&#xff0c;在必要的时候还能随时切换nodejs版本。那么nvm工具你值得…

[oeasy]python0073_进制转化_eval_evaluate_衡量_oct_octal_八进制

进制转化 回忆上次内容 上次了解的是 整型数字类变量integer前缀为i 整型变量 和 字符串变量 不同 整型变量 是 直接存储二进制形式的可以用 int()函数 将 2进制形式的 字符串 转化为 十进制整数 int()函数 接受两个变量 待转化的字符串字符串使用的进制 二进制 和 十进制…

No spring.config.import property has been defined

报错如下&#xff0c;异常信息里面已经提示了修改的方式&#xff1a; 19:16:46.221 [main] ERROR org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter - *************************** APPLICATION FAILED TO START ***************************Description…

数据库应用:MySQL高级语句(三)存储过程

目录 一、理论 1.存储过程 2.存储过程操作 3.存储过程的参数 4.存储过程的控制语句 二、实验 1.创建和调用存储过程 ​编辑 ​编辑 2.存储过程的参数 3.存储过程的控制语句 三、总结 一、理论 1.存储过程 &#xff08;1&#xff09;概念 存储过程是一组为了完成…