# Context Menu
Links: [Home](../index.md)
An absolutely positioned menu built from an array of options -- text, icon, an action (function or URL), nested sub-lists, a custom render hook, style classes and a draggable "headbar" item -- shown at the mouse position (or anywhere) and hidden on the next `mouseup` outside it. Written by SGApps.IO.
## Loading the Module
```js
Application.require("elements/context-menu").then(function (ContextMenu) {
var menu = ContextMenu(); // one menu instance, reusable
element.addEventListener('contextmenu', function (ev) {
ev.preventDefault();
menu.load(options, { file: 'report.docx' }); // options + a context object
menu.show('mouse-viewport'); // at the mouse, kept inside the viewport
});
});
```
**Dependencies:** `uri-load`, `extensions/prototype` (`fn.mouse.position()`, `fn.window.sizeActive()`, the `_()` DOM chain). The module links `elements/context-menu/style.css` when it loads (z-index, item padding, icon placement, the headbar and move cursors). The menu's list layout -- nested `
` shown on hover, borders, the sub-menu arrow -- comes from the `.rr-menu.rr-menu-vertical` rules of the SGApps **RegalRoyal** theme; without the theme the nested lists are always visible and the menu is a plain white block.
## Markup
```html
```
## API
### `ContextMenu()`
Returns the **menu**, an `ApplicationPrototype`. The constructor creates the node, renders a two-item demo menu ("About" / "Author") into it, adds a `mouseup` listener on `document.body` that hides the menu when `autoHide()` is on, and a `mouseup` listener on the menu node that stops propagation (so a release on the menu never hides it). The demo "About" item references an icon named `user-home`, which is requested as an image URL once at construction (a harmless 404 unless you have such a file); the first `load()` / `options()` replaces it.
| Method | Returns | Description |
|---|---|---|
| `load(options, context)` | menu | Sets the context object (`vars().context`, passed to every action and render hook), replaces the options and re-renders. The usual entry point |
| `options(options)` | -- | Replaces the options and re-renders (keeps the current context) |
| `show([pos])` | menu | Appends the node to `document.body` (detaching it first if it was elsewhere) and calls `position(pos)`. Without `pos` the menu appears at its **last** position (initially the mouse position captured at construction), so pass one |
| `hide()` | menu | Detaches the node |
| `visible()` | `HTMLElement` or `null` | The node's parent (`document.body` while shown), `null` when hidden |
| `position([pos])` | menu or `{x, y}` | `true`: the current mouse position (page coordinates, as tracked by `extensions/prototype`); `'mouse-viewport'`: the mouse position clamped so that the menu stays at least 10 px inside the window (`windowSize - nodeSize - 10`, measured after `show()`); `{ x, y }`: that page position. Sets `left` / `top` in px and returns the menu. Without an argument returns the last position |
| `autoHide([state])` | boolean | With an argument, sets whether a `mouseup` on the document hides the menu (default `true`); always returns the current value |
| `render()` | -- | Rebuilds the `` from the current options (called by `load()` / `options()`); emits `item:render` per item |
| `node()` | `HTMLElement` | The menu `` (plain method, no hooks) |
| `vars()` | object | The variables bag -- `vars().context` is the context passed to `load()` (plain method) |
| `config()` | object | The live configuration `{ node, vars, autoHide, options }` (plain method) |
`load`, `options`, `show`, `hide`, `visible`, `position`, `autoHide` and `render` carry the framework's `before` / `on` / `after` hooks (`menu.on('afterShow', fn)`, `menu.once('onHide', fn)` -- the built-in move handler uses `once('onHide')` to stop dragging).
### Option object
| Key | Type | Description |
|---|---|---|
| `text` | string | Label (a space when missing) |
| `icon` | string | Image URL, rendered as `
![]()
` before the label (positioned by the stylesheet at 17 px / 17 px, max 24 px high). Items without an icon get the class `contextmenu-list--no-icon` on their label, which reserves the same 20 px |
| `action` | `function (event, context, menu)`, string | A function runs on click with the context from `load()` and the menu (call `menu.hide()` yourself -- the click's `mouseup` lands on the menu and does not auto-hide). A string makes the label an `
` with that `href`. Neither: an inert label with `cursor: default` |
| `list` | Option[] | A nested menu rendered as a `` inside the item (before the label); the theme shows it on hover |
| `render` | `function (item, li, menu, context)` | Called after the item is built -- decorate the `- `, add badges, disable it, ... |
| `style` | string | Space-separated names, each added to the `
- ` as `contextmenu--style--`; `headbar` is styled by the module (grey background) |
| `handle-actions` | string[] | Built-in behaviours attached to the item; the only one is `moveContextMenu`: pressing the mouse on the item and moving drags the whole menu (`cursor: move`, class `contextmenu--action--moveContextMenu`) |
### Events
| Event | Arguments | Description |
|---|---|---|
| `item:render` | `item, li, menu, trigger` | Emitted for every item on each `render()`; `trigger(event)` runs the item's action with the current context |
| `beforeShow` / `onShow` / `afterShow`, `beforeHide` / `onHide` / `afterHide`, ... | method arguments | Lifecycle hooks of the bound methods |
## Use Cases
### A right-click menu on a list of files
```js
Application.require("elements/context-menu").then(function (ContextMenu) {
var menu = ContextMenu();
var options = [
{ text: 'Open', icon: '/icons/open.svg', action: function (ev, ctx, m) { openFile(ctx.file); m.hide(); } },
{ text: 'Rename', action: function (ev, ctx, m) { renameFile(ctx.file); m.hide(); } },
{ text: 'Share', list: [
{ text: 'Copy link', action: function (ev, ctx, m) { copyLink(ctx.file); m.hide(); } },
{ text: 'E-mail', action: 'mailto:?subject=File' }
] },
{ text: 'Delete', action: function (ev, ctx, m) { deleteFile(ctx.file); m.hide(); },
render: function (item, li) { li.style.color = 'crimson'; } }
];
document.getElementById('files').addEventListener('contextmenu', function (ev) {
var row = ev.target.closest('[data-file]');
if (!row) return;
ev.preventDefault();
menu.load(options, { file: row.getAttribute('data-file') }).show('mouse-viewport');
});
});
```
### A movable tool palette
```js
var palette = ContextMenu();
palette.autoHide(false); // stays until hidden explicitly
palette.load([
{ text: 'Tools', style: 'headbar', 'handle-actions': ['moveContextMenu'] }, // drag handle
{ text: 'Pencil', action: function () { setTool('pencil'); } },
{ text: 'Eraser', action: function () { setTool('eraser'); } },
{ text: 'Close', action: function (ev, ctx, m) { m.hide(); } }
], {}).show({ x: 20, y: 120 });
```
### Disabling items from `render`
```js
{ text: 'Paste', action: paste, render: function (item, li, menu, context) {
if (!context.clipboard) {
li.style.opacity = '.4';
li.style.pointerEvents = 'none';
}
} }
```
## Notes
- The menu is one node appended to `document.body` with `position: absolute` and page coordinates: it scrolls with the page, which is what `fn.mouse.position()` (page coordinates) expects. `'mouse-viewport'` clamps against the window size, so on a scrolled page the clamped result is only exact near the top-left of the document.
- `show()` on a `contextmenu` event works on both mousedown- (Linux) and mouseup-driven (Windows) platforms because the menu is placed under the cursor: the release lands on the menu, whose `mouseup` handler stops the propagation that would auto-hide it. When you open the menu **away** from the pointer, open it from a `click` (after the `mouseup`) or call `autoHide(false)`.
- Clicking an item does not hide the menu by itself -- call `menu.hide()` in the action.
- The default options rendered at construction contain the SGApps credits ("part of application-prototype", "Author: Sergiu Gordienco"); they are visible only if you `show()` the menu before the first `load()`.
- Live demo: [playground](playground.html#module=context-menu ':ignore :target=_blank').