Skip to content

Instantly share code, notes, and snippets.

@movahhedi
Created June 5, 2023 13:13
Show Gist options
  • Save movahhedi/1c0813ea8ddcc33309f7729d87cb68a2 to your computer and use it in GitHub Desktop.
Save movahhedi/1c0813ea8ddcc33309f7729d87cb68a2 to your computer and use it in GitHub Desktop.
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