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


    目录

    前言

    设计思路

    BaseElem

    Menu

    CustomElement

    BaseDrag

    Drag

    Resize

    最终效果

    总结

    相关代码


    前言

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

    设计思路

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

    BaseElem

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

    1. /**
    2. * 自定义标签的基类
    3. */
    4. class BaseElem extends MessageCenter {
    5. root: HTMLElement = document.body
    6. remove(ele: IParentElem) {
    7. ele?.parentNode?.removeChild(ele)
    8. }
    9. moveTo({ x, y }: { x?: number, y?: number }, ele: IParentElem) {
    10. if (!ele) return
    11. ele.style.left = `${x}px`
    12. ele.style.top = `${y}px`
    13. }
    14. }

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

    1. type MenuListItem = {
    2. label: string
    3. name?: string
    4. handler?(e: MouseEvent): void
    5. }

    菜单类

    1. export class Menu extends BaseElem {
    2. constructor(public menuList: MenuListItem[] = [], public menu?: HTMLElement) {
    3. super()
    4. this.root.addEventListener("contextmenu", this.menuHandler)
    5. }
    6. /**
    7. * 创建菜单函数
    8. * @param e
    9. */
    10. menuHandler = (e: MouseEvent) => {
    11. e.preventDefault();// 取消默认事件
    12. this.remove(this.menu)
    13. this.create(this.root)
    14. this.moveTo({
    15. x: e.clientX,
    16. y: e.clientY
    17. }, this.menu)
    18. this.renderMenuList()
    19. }
    20. /**
    21. * 创建菜单元素
    22. * @param parent 父元素
    23. */
    24. create(parent: HTMLElement) {
    25. this.menu = createElement({
    26. ele: "ul",
    27. attr: { id: "menu" },
    28. parent
    29. })
    30. }
    31. /**
    32. * 菜单列表
    33. * @param list 列表数据
    34. * @param parent 父元素
    35. * @returns
    36. */
    37. renderMenuList(list: MenuListItem[] = this.menuList, parent: IParentElem = this.menu) {
    38. if (!parent) return
    39. list.forEach(it => this.renderMenuListItem(it, parent))
    40. }
    41. /**
    42. * 菜单列表子项
    43. * @param item 单个列表数据
    44. * @param parent 父元素
    45. * @returns 列表子项
    46. */
    47. renderMenuListItem(item: MenuListItem, parent: HTMLElement) {
    48. const li = createElement({
    49. ele: "li",
    50. attr: {
    51. textContent: item.label
    52. },
    53. parent
    54. })
    55. li.addEventListener("click", item.handler ?? noop)
    56. return item
    57. }
    58. }

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

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>Menutitle>
    8. <style>
    9. html,
    10. body {
    11. width: 100%;
    12. height: 100%;
    13. }
    14. #menu {
    15. z-index: 2;
    16. position: fixed;
    17. width: 100px;
    18. min-height: 40px;
    19. background: lightcoral;
    20. }
    21. #menu li {
    22. text-align: center;
    23. line-height: 30px;
    24. cursor: pointer;
    25. }
    26. #menu li:hover {
    27. background: lightblue;
    28. }
    29. style>
    30. head>
    31. <body>
    32. <script type="module">
    33. import { Menu } from "./index.js"
    34. // 初始化菜单功能
    35. const menu = new Menu([
    36. {
    37. label: "关闭", handler: (e) => {
    38. menu.remove(menu.menu)
    39. }
    40. }
    41. ])
    42. script>
    43. body>
    44. html>

    效果如下 

    CustomElement

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

    create:新建标签

    cloneNode:复制标签

    removeEle:删除标签

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

    setCount:标签的计数器

    1. export class CustomElement extends BaseElem {
    2. selectClass = "custom-box"// 未被选中标签class值
    3. private _selectEle: ICustomElementItem = null// 当前选中的标签
    4. count: number = 0// 计数器,区分标签
    5. constructor() {
    6. super()
    7. document.onselectstart = () => false// 取消文字选中
    8. }
    9. /**
    10. * 选中标签后的样式变化
    11. */
    12. set selectEle(val: ICustomElementItem) {
    13. const { _selectEle } = this
    14. this.resetEleClass()
    15. if (val && val !== _selectEle) {
    16. val.className = `select ${this.selectClass}`
    17. this._selectEle = val
    18. }
    19. }
    20. get selectEle() {
    21. return this._selectEle
    22. }
    23. /**
    24. * 初始化事件
    25. * @param ele
    26. */
    27. initEve = (ele: HTMLElement) => {
    28. ele.addEventListener("dblclick", this.select)
    29. }
    30. /**
    31. * 复制标签时增加复制文本标识
    32. * @param elem
    33. */
    34. setCount(elem: HTMLElement) {
    35. elem.textContent += "(copy)"
    36. ++this.count
    37. }
    38. /**
    39. * 选中标签后重置上一个标签的样式
    40. * @returns
    41. */
    42. resetEleClass() {
    43. if (!this._selectEle) return
    44. this._selectEle.className = this.selectClass
    45. this._selectEle = null
    46. }
    47. /**
    48. * 新建标签
    49. * @returns 标签对象
    50. */
    51. create() {
    52. const ele = createElement({
    53. ele: "div",
    54. attr: { className: this.selectClass, textContent: (++this.count).toString() },
    55. parent: this.root
    56. })
    57. return ele
    58. }
    59. /**
    60. * 初始化标签
    61. * @param e 鼠标事件
    62. * @param elem 标签对象
    63. */
    64. add(e: MouseEvent, elem?: HTMLElement) {
    65. const ele = elem ?? this.create()
    66. ele && this.initEve(ele)
    67. this.moveTo({
    68. x: e.clientX,
    69. y: e.clientY
    70. }, ele)
    71. }
    72. /**
    73. * 复制标签操作
    74. * @param e 鼠标事件
    75. * @returns
    76. */
    77. cloneNode(e: MouseEvent) {
    78. if (!this.selectEle) return
    79. const _elem = this.selectEle?.cloneNode?.(true) as HTMLElement
    80. _elem && this.root.appendChild(_elem)
    81. _elem && this.setCount(_elem)
    82. this.add(e, _elem)
    83. this.selectEle = _elem
    84. }
    85. /**
    86. * 删除标签
    87. * @returns
    88. */
    89. removeEle() {
    90. if (!this.selectEle) return
    91. this.remove(this.selectEle as IParentElem)
    92. this.selectEle = null
    93. --this.count
    94. }
    95. /**
    96. * 选中/取消选中标签
    97. * @param e
    98. */
    99. select = (e: MouseEvent) => {
    100. this.selectEle = e.target
    101. }
    102. /**
    103. * 点击body时取消选中(未使用)
    104. * @param e
    105. */
    106. unselected = (e: MouseEvent) => {
    107. if (e.target === this.root) this.selectEle = null
    108. }
    109. }

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

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>Menutitle>
    8. <style>
    9. html,
    10. body {
    11. width: 100%;
    12. height: 100%;
    13. }
    14. #menu {
    15. z-index: 2;
    16. position: fixed;
    17. width: 100px;
    18. min-height: 40px;
    19. background: lightcoral;
    20. }
    21. #menu li {
    22. text-align: center;
    23. line-height: 30px;
    24. cursor: pointer;
    25. }
    26. #menu li:hover {
    27. background: lightblue;
    28. }
    29. .custom-box {
    30. line-height: 100px;
    31. text-align: center;
    32. width: 100px;
    33. height: 100px;
    34. white-space: nowrap;
    35. text-overflow: ellipsis;
    36. overflow: hidden;
    37. background: lightgreen;
    38. position: fixed;
    39. cursor: move;
    40. }
    41. .select {
    42. z-index: 1;
    43. border: 3px solid black;
    44. }
    45. style>
    46. head>
    47. <body>
    48. <script type="module">
    49. import { Menu, CustomElement } from "./index.js"
    50. // 初始化标签
    51. const elem = new CustomElement()
    52. // 初始化菜单功能
    53. const menu = new Menu([
    54. {
    55. label: "新建", handler: (e) => {
    56. menu.remove(menu.menu)
    57. elem.add(e)
    58. }
    59. }, {
    60. label: "复制", handler: (e) => {
    61. menu.remove(menu.menu)
    62. elem.cloneNode(e)
    63. }
    64. }, {
    65. label: "删除", handler: (e) => {
    66. menu.remove(menu.menu)
    67. elem.removeEle()
    68. }
    69. },
    70. {
    71. label: "关闭", handler: (e) => {
    72. menu.remove(menu.menu)
    73. }
    74. }
    75. ])
    76. script>
    77. body>
    78. html>

    效果如下

    BaseDrag

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

    1. /**
    2. * 拖拽基类
    3. */
    4. class BaseDrag extends BaseElem {
    5. constructor(public elem: HTMLElement, public root: any = document) {
    6. super()
    7. this.init()
    8. }
    9. /**
    10. * 初始化事件
    11. */
    12. init() {
    13. this.elem.onmousedown = this.__mouseHandler//添加点击事件,避免重复定义
    14. }
    15. /**
    16. * 将一些公共函数在基类中实现
    17. * @param e 事件对象
    18. */
    19. private __mouseHandler = (e: Partial) => {
    20. const { type } = e
    21. if (type === "mousedown") {
    22. this.root.addEventListener("mouseup", this.__mouseHandler);
    23. this.root.addEventListener("mousemove", this.__mouseHandler);
    24. } else if (type === "mouseup") {
    25. this.root.removeEventListener("mouseup", this.__mouseHandler);
    26. this.root.removeEventListener("mousemove", this.__mouseHandler);
    27. }
    28. type && this.emit(type, e)// 触发子类的函数,进行后续操作
    29. }
    30. /**
    31. * 取消拖拽
    32. */
    33. reset() {
    34. this.elem.onmousedown = null
    35. }
    36. }

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

    Drag

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

    1. /**
    2. * 拖拽调整标签位置
    3. */
    4. export class Drag extends BaseDrag {
    5. offset?: Partial<{ x: number, y: number }>// 鼠标点击时在元素上的位置
    6. constructor(public elem: HTMLElement) {
    7. super(elem)
    8. this.on("mousedown", this.mouseHandler)
    9. this.on("mousemove", this.mouseHandler)
    10. }
    11. /**
    12. * 鼠标事件处理函数,当鼠标按下时,记录鼠标点击时在元素上的位置;当鼠标移动时,根据鼠标位置的变化计算新的位置,并通过调用父类的moveTo方法来移动元素
    13. * @param e
    14. */
    15. mouseHandler = (e: Partial) => {
    16. const { type, target, clientX = 0, clientY = 0 } = e
    17. if (type === "mousedown") {
    18. this.offset = {
    19. x: e.offsetX,
    20. y: e.offsetY
    21. }
    22. } else if (type === "mousemove") {
    23. const { x = 0, y = 0 } = this.offset ?? {}
    24. this.moveTo({
    25. x: clientX - x,
    26. y: clientY - y
    27. }, target as HTMLElement)
    28. }
    29. }
    30. }

    Resize

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

    1. /**
    2. * 拖拽调整标签尺寸
    3. */
    4. export class Resize extends BaseDrag {
    5. startX?: number
    6. startY?: number
    7. startWidth?: IStyleItem
    8. startHeight?: IStyleItem
    9. constructor(public elem: HTMLElement) {
    10. super(elem)
    11. this.on("mousedown", this.mouseHandler)
    12. this.on("mousemove", this.mouseHandler)
    13. }
    14. /**
    15. * 获取标签样式项
    16. * @param ele 标签
    17. * @param key 样式属性名
    18. * @returns 样式属性值
    19. */
    20. getStyle(ele: Element, key: keyof CSSStyleDeclaration) {
    21. const styles = document.defaultView?.getComputedStyle?.(ele)
    22. if (styles && typeof styles[key] === "string") return parseInt(styles[key] as string, 10)
    23. }
    24. /**
    25. * 鼠标事件处理函数,用于处理鼠标按下和移动事件。当鼠标按下时,记录起始位置和当前宽度、高度的值。当鼠标移动时,根据鼠标位置的变化计算新的宽度和高度,并更新元素的样式。
    26. * @param e
    27. */
    28. mouseHandler = (e: Partial) => {
    29. const { type, clientX = 0, clientY = 0 } = e
    30. if (type === "mousedown") {
    31. this.startX = clientX;
    32. this.startY = clientY;
    33. this.startWidth = this.getStyle(this.elem, "width")
    34. this.startHeight = this.getStyle(this.elem, "height")
    35. } else if (type === "mousemove") {
    36. const width = <number>this.startWidth + (clientX - <number>this.startX);
    37. const height = <number>this.startHeight + (clientY - <number>this.startY);
    38. this.elem.style.width = width + 'px';
    39. this.elem.style.height = height + 'px';
    40. }
    41. }
    42. }

    最终效果

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

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>Menutitle>
    8. <style>
    9. html,
    10. body {
    11. width: 100%;
    12. height: 100%;
    13. }
    14. #menu {
    15. z-index: 2;
    16. position: fixed;
    17. width: 100px;
    18. min-height: 40px;
    19. background: lightcoral;
    20. }
    21. #menu li {
    22. text-align: center;
    23. line-height: 30px;
    24. cursor: pointer;
    25. }
    26. #menu li:hover {
    27. background: lightblue;
    28. }
    29. .custom-box {
    30. line-height: 100px;
    31. text-align: center;
    32. width: 100px;
    33. height: 100px;
    34. white-space: nowrap;
    35. text-overflow: ellipsis;
    36. overflow: hidden;
    37. background: lightgreen;
    38. position: fixed;
    39. cursor: move;
    40. }
    41. .select {
    42. z-index: 1;
    43. border: 3px solid black;
    44. }
    45. style>
    46. head>
    47. <body>
    48. <script type="module">
    49. import { Menu, CustomElement, Drag, Resize } from "./index.js"
    50. // 初始化标签
    51. const elem = new CustomElement()
    52. // 初始化菜单功能
    53. const menu = new Menu([
    54. {
    55. label: "新建", handler: (e) => {
    56. menu.remove(menu.menu)
    57. elem.add(e)
    58. }
    59. }, {
    60. label: "复制", handler: (e) => {
    61. menu.remove(menu.menu)
    62. elem.cloneNode(e)
    63. }
    64. }, {
    65. label: "删除", handler: (e) => {
    66. menu.remove(menu.menu)
    67. elem.removeEle()
    68. }
    69. }, {
    70. label: "调整位置", handler: (e) => {
    71. menu.remove(menu.menu)
    72. elem.selectEle && (elem.selectEle.__drag = new Drag(elem.selectEle))
    73. }
    74. }, {
    75. label: "调整大小", handler: (e) => {
    76. menu.remove(menu.menu)
    77. elem.selectEle && (elem.selectEle.__resize = new Resize(elem.selectEle))
    78. }
    79. }, {
    80. label: "取消拖拽", handler: (e) => {
    81. menu.remove(menu.menu)
    82. elem.selectEle?.__drag?.reset?.()
    83. elem.selectEle?.__resize?.reset?.()
    84. }
    85. },
    86. {
    87. label: "关闭", handler: (e) => {
    88. menu.remove(menu.menu)
    89. }
    90. }
    91. ])
    92. script>
    93. body>
    94. html>

     

    总结

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

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

    相关代码

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

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

  • 相关阅读:
    软件架构模式
    计算机网络原理 运输层
    C++11智能指针学习笔记及拓展
    前端开发规范总结
    尚医通_第12章_用户平台首页数据
    【JavaWeb】JSP(172-190)
    一文拿下HTTP
    图像切分:将一张长图片切分为指定长宽的多张图片
    RunnerGo:让你的性能测试变得轻松简单
    leetcode-62.不同路径
  • 原文地址:https://blog.csdn.net/time_____/article/details/131465364