Instantly share code, notes, and snippets.
Created
June 5, 2023 13:13
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save movahhedi/1c0813ea8ddcc33309f7729d87cb68a2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type ContextMenuItem = { | |
content: string; | |
divider?: "top" | "bottom" | "top-bottom"; | |
events?: { | |
[key in keyof HTMLElementEventMap]?: | |
(this: HTMLButtonElement, ev: HTMLElementEventMap[keyof HTMLElementEventMap]) => any | |
} | |
} | |
interface ContextMenuConstructor { | |
target: string; | |
menuItems: ContextMenuItem[]; | |
mode?: "light" | "dark"; | |
} | |
export class ContextMenu { | |
private target: string; | |
private menuItems: ContextMenuItem[]; | |
private mode: "light" | "dark"; | |
private targetNode: NodeList | []; | |
private menuItemsNode: HTMLLIElement[]; | |
private isOpened: boolean = false; | |
private menuContainer: HTMLUListElement; | |
constructor({ target, menuItems, mode = "dark" }: ContextMenuConstructor) { | |
this.target = target; | |
this.mode = mode; | |
this.targetNode = document.querySelectorAll(this.target); | |
if (!this.targetNode) { | |
console.error(`ContextMenu :: getTargetNode :: "${this.target}" target not found`); | |
this.targetNode = []; | |
} | |
this.menuItems = menuItems; | |
if (!this.menuItems) { | |
console.error("ContextMenu :: getMenuItemsNode :: Please enter menu items"); | |
this.menuItems = []; | |
} | |
this.menuItemsNode = this.menuItems.map((data, index) => { | |
const item = this.createItemMarkup(data); | |
(item.firstChild as HTMLElement)?.setAttribute("style", `animation-delay: ${index * 0.08}s`); | |
return item; | |
}); | |
this.menuContainer = document.createElement("ul"); | |
this.menuContainer.classList.add("contextMenu"); | |
this.menuContainer.setAttribute("data-theme", this.mode); | |
this.menuItemsNode.forEach((item) => this.menuContainer.appendChild(item)); | |
} | |
createItemMarkup(data: ContextMenuItem) { | |
const button = document.createElement("button"); | |
const item = document.createElement("li"); | |
button.innerHTML = data.content; | |
button.classList.add("contextMenu-button"); | |
item.classList.add("contextMenu-item"); | |
if (data.divider) item.setAttribute("data-divider", data.divider); | |
item.appendChild(button); | |
if (data.events) { | |
Object.entries(data.events).forEach(([key, value]) => { | |
button.addEventListener(key, value); | |
}); | |
} | |
return item; | |
} | |
closeMenu() { | |
if (this.isOpened) { | |
this.isOpened = false; | |
this.menuContainer.remove(); | |
} | |
} | |
init() { | |
document.addEventListener("click", () => this.closeMenu()); | |
window.addEventListener("blur", () => this.closeMenu()); | |
document.addEventListener("contextmenu", (e) => { | |
this.targetNode.forEach((target) => { | |
if (!(e.target as HTMLElement)?.contains(target)) { | |
this.menuContainer.remove(); | |
} | |
}); | |
}); | |
this.targetNode.forEach((target) => { | |
target.addEventListener("contextmenu", (e) => { | |
e.preventDefault(); | |
this.isOpened = true; | |
const { clientX, clientY } = e; | |
document.body.appendChild(this.menuContainer); | |
const positionY = | |
clientY + this.menuContainer.scrollHeight >= window.innerHeight | |
? window.innerHeight - this.menuContainer.scrollHeight - 20 | |
: clientY; | |
const positionX = | |
clientX + this.menuContainer.scrollWidth >= window.innerWidth | |
? window.innerWidth - this.menuContainer.scrollWidth - 20 | |
: clientX; | |
this.menuContainer.setAttribute( | |
"style", | |
`--width: ${this.menuContainer.scrollWidth}px; | |
--height: ${this.menuContainer.scrollHeight}px; | |
--top: ${positionY}px; | |
--left: ${positionX}px;` | |
); | |
}); | |
}); | |
} | |
} | |
const copyIcon = "<svg viewBox=\"0 0 24 24\" width=\"13\" height=\"13\" stroke=\"currentColor\" stroke-width=\"2.5\" style=\"margin-right: 7px\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"css-i6dzq1\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>"; | |
const cutIcon = "<svg viewBox=\"0 0 24 24\" width=\"13\" height=\"13\" stroke=\"currentColor\" stroke-width=\"2.5\" style=\"margin-right: 7px\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"css-i6dzq1\"><circle cx=\"6\" cy=\"6\" r=\"3\"></circle><circle cx=\"6\" cy=\"18\" r=\"3\"></circle><line x1=\"20\" y1=\"4\" x2=\"8.12\" y2=\"15.88\"></line><line x1=\"14.47\" y1=\"14.48\" x2=\"20\" y2=\"20\"></line><line x1=\"8.12\" y1=\"8.12\" x2=\"12\" y2=\"12\"></line></svg>"; | |
const pasteIcon = "<svg viewBox=\"0 0 24 24\" width=\"13\" height=\"13\" stroke=\"currentColor\" stroke-width=\"2.5\" style=\"margin-right: 7px; position: relative; top: -1px\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"css-i6dzq1\"><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"></path><rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\" ry=\"1\"></rect></svg>"; | |
const downloadIcon = "<svg viewBox=\"0 0 24 24\" width=\"13\" height=\"13\" stroke=\"currentColor\" stroke-width=\"2.5\" style=\"margin-right: 7px; position: relative; top: -1px\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"css-i6dzq1\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path><polyline points=\"7 10 12 15 17 10\"></polyline><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line></svg>"; | |
const deleteIcon = "<svg viewBox=\"0 0 24 24\" width=\"13\" height=\"13\" stroke=\"currentColor\" stroke-width=\"2.5\" fill=\"none\" style=\"margin-right: 7px\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"css-i6dzq1\"><polyline points=\"3 6 5 6 21 6\"></polyline><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"></path></svg>"; | |
const menuItems: ContextMenuItem[] = [ | |
{ | |
content: `${copyIcon}Copy`, | |
events: { | |
click: (e) => console.log(e, "Copy Button Click"), | |
// mouseover: () => console.log("Copy Button Mouseover") | |
// You can use any event listener from here | |
} | |
}, | |
{ content: `${pasteIcon} Paste` }, | |
{ content: `${cutIcon} Cut` }, | |
{ content: `${downloadIcon} Download` }, | |
{ | |
content: `${deleteIcon} Delete`, | |
divider: "top" | |
} | |
]; | |
const dark = new ContextMenu({ | |
target: ".target-dark", | |
menuItems | |
}); | |
dark.init(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment