No description
  • TypeScript 82.5%
  • SCSS 16%
  • HTML 1%
  • JavaScript 0.5%
Find a file
2026-06-27 09:32:38 +02:00
.vscode Upgraded to tsdown and exported types 2026-04-07 16:53:50 +02:00
src Made lines lazy 2026-06-26 21:06:30 +02:00
.gitignore Initial commit 2026-02-12 13:58:40 +01:00
eslint.config.js Updated dependencies, ci and tsdown 2026-04-14 14:46:02 +02:00
index.html Initial commit 2026-02-12 13:58:40 +01:00
package-lock.json Made lines lazy 2026-06-26 21:06:30 +02:00
package.json Added Readme 2026-06-27 09:32:38 +02:00
README.md Added Readme 2026-06-27 09:32:38 +02:00
tsconfig.json Initial commit 2026-02-12 13:58:40 +01:00
tsdown.config.ts Updated dependencies, ci and tsdown 2026-04-14 14:46:02 +02:00

@chocbite/ts-lib-context-menu

Framework-agnostic TypeScript library for right-click / touch-and-hold context menus built on native Web Components.

Installation

npm install @chocbite/ts-lib-context-menu

Basic Usage

Import the library and attach a context menu to an element. The CSS is auto-injected on import.

import ctm from "@chocbite/ts-lib-context-menu";

ctm.attach(
  document.getElementById("editor")!,
  ctm.menu([
    ctm.line("Cut", () => document.execCommand("cut")),
    ctm.line("Copy", () => document.execCommand("copy")),
    ctm.line("Paste", () => document.execCommand("paste")),
  ]),
);

Right-click (or long-press) the element to open the menu.

API

ctm.menu(lines)

Creates a menu panel. lines can be a static array, a lazy factory, or a Promise/async factory (shows a spinner while loading).

// Static array
const menu = ctm.menu([
  ctm.line("Option A", () => console.log("A")),
  ctm.line("Option B", () => console.log("B")),
]);

// Factory — evaluated on every open
const menu = ctm.menu(() => [
  ctm.line("Timestamp", () => console.log(Date.now())),
]);

// Async factory — shows a spinner until the Promise resolves
const menu = ctm.menu(async () => {
  const items = await fetchMenuItems();
  return items.map((item) => ctm.line(item.label, item.action));
});

ctm.line(text, func, icon?, shortcut?, checkmark?)

Creates a clickable menu item.

import { material_delete_rounded } from "@chocbite/ts-lib-icons";

ctm.line("Delete", () => deleteFile());
ctm.line("Delete", () => deleteFile(), material_delete_rounded());
ctm.line("Save", () => save(), undefined, "Ctrl+S");
ctm.line("Mute", () => toggleMute(), undefined, undefined, isMuted);

ctm.sub(text, menu, icon?)

Creates a submenu trigger. Hover (300 ms) or keyboard (→ / Enter / Space) opens the nested menu.

const colorMenu = ctm.menu([
  ctm.line("Red", () => setColor("red")),
  ctm.line("Green", () => setColor("green")),
  ctm.line("Blue", () => setColor("blue")),
]);

const menu = ctm.menu([
  ctm.line("Copy", () => copy()),
  ctm.sub("Color", colorMenu),
  ctm.line("Delete", () => remove()),
]);

ctm.devider()

Inserts a horizontal separator.

const menu = ctm.menu([
  ctm.line("Cut", () => cut()),
  ctm.line("Copy", () => copy()),
  ctm.devider(),
  ctm.line("Delete", () => remove()),
]);

ctm.attach(element, lines, block?)

Binds a context menu to a DOM element.

// Static menu
ctm.attach(el, menu);

// Dynamic menu — factory called on each open
import { some, none } from "@chocbite/ts-lib-result";

ctm.attach(el, () =>
  some(
    ctm.menu([
      ctm.line("Edit", () => edit(el)),
      ctm.line("Delete", () => remove(el)),
    ]),
  ),
);

// Conditionally suppress the menu
ctm.attach(el, menu, () => el.dataset.readonly === "true");

// Return None to suppress for a specific open
ctm.attach(el, () => (isLocked(el) ? none() : some(menu)));

ctm.detach(element)

Removes a previously attached context menu.

ctm.detach(el);

ctm.summon(menu, element?, x?, y?, dont_cover?)

Opens a menu programmatically — no right-click needed.

// At specific coordinates, in main document
ctm.summon(menu, undefined, event.clientX, event.clientY);

// Centered on an element, in same document as buttonEl
ctm.summon(menu, buttonEl);

// Positioned beside an element, in same document as buttonEl, without covering it
ctm.summon(menu, buttonEl, undefined, undefined, true);

ctm.default(lines | false)

Sets a document-wide fallback context menu shown on any right-click with no element-specific menu.

// Apply a global fallback
ctm.default(
  ctm.menu([
    ctm.line("View source", () => window.open("view-source:" + location.href)),
    ctm.line("Inspect", () => console.log("inspect")),
  ]),
);

// Suppress the browser's native context menu everywhere
ctm.default(false);

ctm.has(element)

Returns true if a menu is attached to the element.

if (!ctm.has(el)) {
  ctm.attach(el, menu);
}

Conditional Items

undefined values in a lines array are silently skipped, making conditional items clean:

ctm.menu(() => [
  ctm.line("Edit", () => edit()),
  canDelete ? ctm.line("Delete", () => remove()) : undefined,
  isAdmin ? ctm.line("Admin Panel", () => openAdmin()) : undefined,
]);

Theming

Theming via @chocbite/ts-lib-theme.

License

MIT