目录
分享一下之前公司实现自定义菜单的思路,禁用浏览器右键菜单,使用自定义的菜单将其代替,主要功能有:鼠标右键调出菜单,双击选中/取消选中标签,新建标签,删除标签,调整位置,调整大小,取消拖拽,关闭菜单
自定义标签基类提供了移动和删除标签功能,它充当公共类的作用,后面的自定义标签都继承与该类
- /**
- * 自定义标签的基类
- */
- 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`
- }
- }
菜单类的作用是创建自定义菜单,代替浏览器原有的右键菜单。其中每个菜单子项的数据结构如下
- 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设置点击事件
- 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>Menutitle>
- <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>
效果如下
为了让菜单与被控标签解耦(实际上也没有联系),使用新的类承载标签管理。其中自定义标签主要包含以下功能:
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
- }
- }
结合上述类的实现,我们在页面中增加几种菜单
- 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>Menutitle>
- <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>
效果如下
完成上述基础功能后,我们可以尝试对标签位置和大小进行修改,所以我们建立一个鼠标拖拽的基类,用来实现拖拽的公共函数
- /**
- * 拖拽基类
- */
- 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
) => { - 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函数中我们对鼠标事件进行了拦截,并且借助消息中心将事件传递出去,方便后续的拓展
接着是拖拽移动标签的功能,该类拖拽了鼠标按下和移动的回调
- /**
- * 拖拽调整标签位置
- */
- 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
) => { - 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)
- }
- }
- }
最后我们将位置改成高度宽度,实现一下调整标签尺寸的类
- /**
- * 拖拽调整标签尺寸
- */
- 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
) => { - 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中使用上述的所有功能,演示一下全部功能
- 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>Menutitle>
- <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,让我们能够创建具有定制化选项和交互性的菜单。文章主要介绍了前端自定义菜单的实现过程,描述了创建标签、选中标签、复制标签、删除标签、拖拽位置及大小等功能。
以上就是文章全部内容了,感谢你看到了最后,如果觉得不错的话,请给个三连支持一下吧,谢谢!