前言
分享一下之前公司实现自定义菜单的思路,禁用浏览器右键菜单,使用自定义的菜单将其代替,主要功能有:鼠标右键调出菜单,双击选中/取消选中标签,新建标签,删除标签,调整位置,调整大小,取消拖拽,关闭菜单
设计思路
- 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,让我们能够创建具有定制化选项和交互性的菜单。文章主要介绍了前端自定义菜单的实现过程,描述了创建标签、选中标签、复制标签、删除标签、拖拽位置及大小等功能。
以上就是文章全部内容了,感谢你看到了最后,如果觉得不错的话,请给个三连支持一下吧,谢谢!