JS case: implement custom menu in browser

Table of contents

foreword

Design ideas

BaseElem

CustomElement

BaseDrag

Drag

Resize

final effect

Summarize

related code


foreword

Let me share the company's idea of ​​implementing a custom menu before, disable the right-click menu of the browser, and replace it with a custom menu. The main functions are: right-click to bring up the menu, double-click to select/unselect a label, create a new label, delete a label, adjust position, resize, cancel dragging, and close the menu.

Design ideas

  • MessageCenter comes from the message center and provides basic communication functions for components
  • BaseElem: The base class of custom tags, providing some common methods and properties, inherited from MessageCenter, and communicating externally through the message center
  • Menu: Menu class, used to create and display custom menus. It inherits from BaseElem and implements methods such as creating menus and rendering menu lists
  • CustomElement: A custom element class for creating and manipulating custom labels. It inherits from BaseElem and provides methods for creating labels, selecting labels, copying labels, deleting labels, etc.
  • BaseDrag: The base class for dragging, which provides basic dragging functions. It inherits from BaseElem, realizes the processing and triggering of mouse events
  • Drag: Drag and drop to adjust the label position class, inherited from BaseDrag, to achieve the function of dragging the label position
  • Resize: Drag and drop to adjust the label size class, inherited from BaseDrag, to realize the function of drag and drop to adjust the label size.

BaseElem

The custom label base class provides the function of moving and deleting labels, which acts as a public class, and subsequent custom labels inherit from this class

/**
 * 自定义标签的基类
 */
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

The function of the menu class is to create a custom menu to replace the original right-click menu of the browser. The data structure of each menu item is as follows

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

menu class

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

We use the menu function in HTML, configure the menu options through the label, and set the click event through the 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>

The effect is as follows 

CustomElement

In order to decouple the menu from the controlled label (in fact, there is no connection), a new class is used to carry the label management. Among them, the custom label mainly includes the following functions:

create: create a new label

cloneNode: copy label

removeEle: remove label

select: select/unselect the label (the function is triggered by double-clicking)

setCount: the counter of the label

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

Combined with the implementation of the above classes, we add several menus to the page

<!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>

The effect is as follows

BaseDrag

After completing the above basic functions, we can try to modify the position and size of the label, so we create a base class for mouse dragging to implement dragging public functions

/**
 * 拖拽基类
 */
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
    }
}

It can be seen that in the __mouseHandler function of the above code, we intercepted the mouse event, and passed the event through the message center to facilitate subsequent expansion

Drag

Then there is the function of dragging and moving the label, which drags the callback of mouse press and movement

/**
 * 拖拽调整标签位置
 */
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

Finally, we change the position to height and width, and implement a class that adjusts the size of the label

/**
 * 拖拽调整标签尺寸
 */
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';
        }
    }
}

final effect

Finally, we use all the above functions in HTML to demonstrate all the functions

<!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>

 

Summarize

When it comes to customizing menus, JavaScript provides rich functionality and APIs that allow us to create menus with customization options and interactivity. The article mainly introduces the implementation process of the front-end custom menu, and describes the functions of creating tags, selecting tags, copying tags, deleting tags, dragging positions and sizes, etc.

The above is the whole content of the article, thank you for reading to the end, if you think it is good, please give a three-link support, thank you!

related code

utils-lib-js: JavaScript tool functions, some commonly used js functions encapsulated

myCode: Some small cases or projects based on js - Gitee.com

Guess you like

Origin blog.csdn.net/time_____/article/details/131465364