{"id":18739637,"url":"https://github.com/smellyshovel/custom-context-menu","last_synced_at":"2025-04-12T20:03:16.951Z","repository":{"id":97056473,"uuid":"104921047","full_name":"smellyshovel/custom-context-menu","owner":"smellyshovel","description":" Improve a web-interface's UX by customizing its context menus. No dependencies. Less than 3kB both files (gzipped and minified).","archived":false,"fork":false,"pushed_at":"2018-05-09T11:24:20.000Z","size":314,"stargazers_count":4,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-26T14:12:26.912Z","etag":null,"topics":["context-menu","contextmenu","menu","right-click","rightclick"],"latest_commit_sha":null,"homepage":"https://smellyshovel.github.com/custom-context-menu/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/smellyshovel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-09-26T18:09:05.000Z","updated_at":"2023-01-14T14:31:42.000Z","dependencies_parsed_at":"2023-04-12T18:16:05.965Z","dependency_job_id":null,"html_url":"https://github.com/smellyshovel/custom-context-menu","commit_stats":{"total_commits":194,"total_committers":1,"mean_commits":194.0,"dds":0.0,"last_synced_commit":"19e4550c4219e73b78e1e5e506f0022311d34bbb"},"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smellyshovel%2Fcustom-context-menu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smellyshovel%2Fcustom-context-menu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smellyshovel%2Fcustom-context-menu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smellyshovel%2Fcustom-context-menu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smellyshovel","download_url":"https://codeload.github.com/smellyshovel/custom-context-menu/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248625493,"owners_count":21135513,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["context-menu","contextmenu","menu","right-click","rightclick"],"created_at":"2024-11-07T15:36:37.975Z","updated_at":"2025-04-12T20:03:16.943Z","avatar_url":"https://github.com/smellyshovel.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Custom Context Menu\nImprove a web-interface's UX by customizing its context menus. _No\ndependencies. Less than 3kB both files (gzipped and minified)._\n\n## Contents\n1. [Installation](#installation)\n1. [Usage](#usage)\n    1. [Link the script](#link-the-script)\n    1. [Define a new Context Menu](#define-a-new-context-menu)\n        * [Target](#target)\n        * [Items](#items)\n        * [Options](#options)\n    1. [Fallback menu](#fallback-menu)\n1. [Sub-menus](#sub-menus)\n1. [Some things you might wish you knew earlier](#some-things-you-might-wish-you-knew-earlier)\n1. [Examples](#examples)\n    1. [Without sub-menus](#without-sub-menus)\n    2. [With 2-levels-nested sub-menu](#with-2-levels-nested-sub-menu)\n1. [Styling](#styling)\n1. [Contribution](#contribution)\n\n## [Installation](#installation)\n1. Using NPM or Yarn\n\n    ```bash\n    $ npm install custom-context-menu --save\n    ```\n\n    ```bash\n    $ yarn add custom-context-menu\n    ```\n\n1. Standalone\n\n    You can also just download a [preferable version](https://github.com/smellyshovel/custom-context-menu/releases) of\n    the package's source and use it for your taste.\n\n## [Usage](#usage)\n\n### [Link the script](#link-the-script)\n\nLink the `src/context-menu.js` or both the `src/context-menu.js` and `src/context-sub-menu.js` if you do also wish to use sub-menus\n\n```html\n\u003cscript src=\"path/to/package/src/context-menu.js\"\u003e\n```\n\nor\n\n```html\n\u003cscript src=\"path/to/package/src/context-menu.js\"\u003e\n```\n\nNotice also that if you are about to use sub-menus then you **must** include the `context-menu.js` **before** the `context-sub-menu.js`.\n\n### [Define a new Context Menu](#define-a-new-context-menu)\n\nThe defenition of a new Context Menu is rather simple. All you have to do is to invoke the `ContextMenu` constructor providing it with 3 arguments: a `target`, an array of `items` and, optionally, an object of `options`\n\n```javascript\nnew ContextMenu(target, items, options);\n```\n\n#### [Target](#target)\n\nThe target is a DOM element interaction with which leads to opening of the Context Menu. Or it can also be a collection of elements. All the following examples are valid\n\n```javascript\nlet target = document.querySelector(\"a#home\");\n```\n\n```javascript\nlet target = document.querySelectorAll(\"div.button\");\n```\n\n```javascript\nlet target = document.getElementById(\"one-and-only\");\n```\n\nThe `target` might also be the `document` which is quite useful for defining a fallback Context Menu\n\n```javascript\nlet target = document;\n```\n\nMore on the fallback menu in the [appropriate section](#fallback-menu).\n\n#### [Items](#items)\n\nThe `items` array is used to define all the items of the Context Menu. Each item is either an object or a string\n\n##### Object\n\nObjects are used to describe normal items like those you can press to trigger some action\n\n```javascript\nlet items = [\n    {\n        title: \"Bring a beer\",\n        action: bringABeer\n    },\n\n    {\n        title: \"Make a sandwich\",\n        action() {\n            let bread = getBread();\n            let butter = getButter();\n            let bacon = getBacon();\n\n            makeSandwich(bread, butter, bacon);\n        }\n    }\n];\n```\n\nEach normal item object must have 2 properties: a `title` which is a name of the item and an `action` which is a function that is gonna be invoked when the item is selected.\n\nHowever, the `action` might also be an insance of the `ContextMenu.Sub`. In such case the item serves as the _caller_ of a sub-menu\n\n```javascript\nlet items = [\n    {\n        title: \"Check me in\",\n        action: new ContextMenu.Sub(items, options)\n    }\n];\n```\n\nMore on sub-menus in the [appropriate section](#).\n\n##### String\nStrings are the special items. For example you might want to separate 2 items with a horizontal bar between them. In order to do so use the special `\"separator\"` item\n\n```javascript\nlet items = [\n    {\n        title: \"Bring a beer\",\n        action: bringABeer\n    },\n\n    \"separator\", // here\n\n    {\n        title: \"Make a sandwich\",\n        action() {\n            let bread = getBread();\n            let butter = getButter();\n            let bacon = getBacon();\n\n            makeSandwich(bread, butter, bacon);\n        }\n    }\n];\n```\n\nAll the special items are predefined. There's currently only one special item - the `separator`, though the list is extensible and will probably become expanded in the future.\n\n#### [Options](#options)\n\nThe `options` object provides the options which define the behavior of the Context Menu. This argument is optional, i.e. you might either provide it or not.\n\n```javascript\nlet options = {\n    name: \"\",\n    disabled: false,\n    nativeOnAlt: true,\n    penetrable: false,\n    transfer: \"y\",\n    verticalSpacing: 10,\n    callback: {\n        opening() {},\n        closure() {}\n    }\n};\n```\n\nThe example above lists all the possible options as well as their default values. If the `options` is not provided then the defaults would be used. The same applies for the lacking options (those that you didn't specified).\n\n##### `name`\nA string holding the name of the Context Menu. It might be anything you like. The option is used purely for styling purposes in order to identify a certain Context Menu among the others.\n\n##### `disabled`\nA boolean indicating whether the Context Menu is disabled or not. If the Context Menu is disabled then right-clicking the `target` won't do anything. For example it might be useful for disabling the browser's native context menu for a certain element.\n\n##### `nativeOnAlt`\nA boolean indicating whether to show the browser's native context menu or not if the `target` has been right-clicked *and* the `alt` key was holded. **Notice**, that the `disabled` option has no influence on behavior of this one, i.e. even if the Context Menu is `disabled` but the `nativeOnAlt` is `true` then if the `target` has been right-clicked during the `alt` key holding the browser's native context menu will appear.\n\n##### `penetrable`\nA boolean indicating whether the overlay of the Context Menu is penetrable for right-clicking \"through\" it or not. If set to `false` then a right click on the overlay will just close the Context Menu. But if set to `true` then a new Context Menu for the appropriate target (if any) will apear right after the closure.\n\n##### `transfer`\nThe option defines what to do with the Context Menu if it can't fit in the viewport. Must have one of 4 values: `\"x\"`, `\"y\"`, `\"both\"` or `false`. Proceed to the [demo](#) to see those in action.\n\n##### `verticalSpacing`\nThe option the value of which must be an iteger represents the amount of pixels to be stepped off a top and a bottom edges of the viewport if the menu is overflowed, i.e. if it can't fit in the viewport vertically. That might be a case on having too much items or too short viewport (e.g. a very small browser window).\n\n##### `callback`\nThe object with 2 properties: `opening` and `closure`, each of which is a function. The function is invoked whenever the menu is opened or closed respectively.\n\n### [Fallback menu](#fallback-menu)\n\nYou may define Custom Context Menus for all the `\u003ca\u003e` elements on a page, for all the `\u003cp\u003e` and `\u003cbutton\u003e` elements. But what about the other stuff? If a user right-clicked not one of these elements, what's then?\n\nWell, you can define a page-wide fallback Context menu, which will be used as the menu for all the elements the other Context Menus are not specified for. In order to do so you have to register a Context Menu with the `target` equal to the `document`\n\n```javascript\nlet fallbackCM = new ContextMenu(document, items);\n```\n\nIf you do also have a ContextMenu defined for all the `\u003ca\u003e` elements\n\n```javascript\nlet aCM = new ContextMenu(document.querySelectorAll(\"a\"), items);\n```\n\nthen if you right-click any `\u003ca\u003e` element the a-element-menu will appear. But if you right-click anywhere else within the page, the fallback one will.\n\nYou may also reach identical behavior by using the `document.documentElement` instead of `document`. However, such approach might have some disatvantages, such as that if the `\u003chtml\u003e` element's (which is represented by the `document.documentElement`) height is less than the height of the viewport then all the \"differential\" part of the page won't serve as a Context Menu caller.\n\n## [Sub-menus](#sub-menus)\n\nIt's quite common to combine similar items into groups and thus sub-menus are your way to go. The sub-menu is a menu within a menu.\n\nA sub-menu must be defined as an action of some item\n\n```javascript\nlet items = [\n    {\n        title: \"Hover me!\",\n        action: new ContextMenu.Sub(items, options)\n    }\n];\n```\n\nThe item that is used to open the sub-menu is called a **caller**.\n\nThe only defference in the process of creation of a sub-menu is that it doesn't accept the `target` as an argument. And this is quite expected. The thing here is that the sub-menu might only be opened using its caller, i.e. the sub-menu is not tied to any DOM element (if not counting the caller itself the element), therefore there's no need in providing a `target` to a sub-menu's constructor.\n\nThe approach of defining the `items` is absolutely similar with the normal Context Menus. However, the `options` are not\n\n```javascript\nlet options = {\n    name: \"\",\n    delay: {\n        opening: 250,\n        closure: 250\n    },\n    transfer: \"x\",\n    verticalSpacing: 10,\n    callback: {\n        opening() {},\n        closure() {}\n    }\n}\n```\n\nHere is the list of all the available for a sub-menu options. As you can see it's quite simiar with the one that is for not-a-sub-menu. It lacks the `disabled`, `nativeOnAlt` and `penetrable` options. The reason is because they are absolutely pointless for sub-menus.\n\nBut the `delay` option is available for sub-meus whilst not for normal Context Menus. The option defines how much time should pass before the sub-menu might be opened or closed after the caller has become selected. The time is specified in milliseconds.\n\n## [Some things you might wish you knew earlier](#tips)\n\n### 1. Context of an action\n\nAn action when invoked gains the context of the Context Menu instance itself\n\n```javascript\nlet fallbackCM = new ContextMenu(document, [\n    {\n        title: \"Luke, I'm your father\",\n        action() {\n            console.log(this === fallbackCM); // true\n        }\n    }\n]);\n```\n\n### 2. `items` and `options` are used without making copies of them\n\nThe `items` array might change during the lifecycle of the page the Context Menu using the array is used on.\n\nThe prototype of the `options` object will be substituted with the other one.\n\n### 3. Use public properties of a Context Menu to dinamically add (or remove) `items` and change `options`\n\nAfter a Context Menu is initialized (the constructor is invoked) you can still make changes to the menu's `items` and `options` by modifying the corresponding properties of the instance\n\n```javascript\n    let awesomeCM = new ContextMenu(target, items, options);\n\n    setTimeout(() =\u003e {\n        awesomeCM.items.push(newItem);\n        awesomeCM.options.transfer = false;\n    }, 10000);\n```\n\nThe example above adds a new item to the `awesomeCM` Context Menu and changes its `transfer` option's property value to `false` in 10 seconds after the Context Menu has become initialized.\n\n## [Examples](#examples)\n\n### [Without sub-menus](#example-without-sub-menus)\n\nAn example of a regular Context Menu without sub-menus, with prohibited native context menus and which says \"bye\" after it has become closed.\n\n```javascript\n    let cmForLinks = new ContextMenu(document.querySelectorAll(\"a\"), [\n        {\n            title: \"Option 1\",\n            action() {alert(\"You selected the option #1\")}\n        },\n\n        {\n            title: \"Option 2\",\n            action() {alert(\"You selected the option #2\")}\n        }\n    ], {\n        nativeOnAlt: false,\n        callback: {\n            opening() {},\n            closure() {alert(\"bye\")}\n        }\n    });\n```\n\n### [With 2-levels-nested sub-menu](#example-with-2-levels-nested-sub-menu)\n\nAn example of a Context Menu, the second item of which opens a sub-menu, the first item of which opens another one.\n\n```javascript\n    let subMenu1stLevel = new ContextMenu.Sub([\n        {\n            title: \"2 \u003e First\",\n            action: new ContextMenu.Sub([\n                {\n                    title: \"3 \u003e First\",\n                    action() {return 2 + 2}\n                },\n\n                {\n                    title: \"3 \u003e Second\",\n                    action() {return 2 - 2}\n                },\n\n                {\n                    title: \"3 \u003e Third\",\n                    action() {return 2 * 2}\n                }\n            ]);\n        },\n\n        \"separator\",\n\n        {\n            title: \"2 \u003e Second\",\n            action: subMenu1stLevel\n        }\n    ]);\n\n    let cmForButtons = new ContextMenu(document.querySelectorAll(\"button\"), [\n        {\n            title: \"1 \u003e First\",\n            action() {return 2 + 2}\n        },\n\n        {\n            title: \"1 \u003e Second\",\n            action: subMenu1stLevel\n        },\n\n        {\n            title: \"1 \u003e Third\",\n            action() {return 2 * 2}\n        }\n    ]);\n```\n\n## [Styling](#styling)\n\nEach Context Menu is represented as a `\u003cdiv\u003e` with the `data-cm` attribute set to the `name` of the Context Menu. For example if you defined a Context Menu as follows\n\n```javascript\nlet cm = new ContextMenu(target, items, {\n    name: \"fallback\"\n});\n```\n\nthen you can easily style it with CSS adressing it by its name\n\n```CSS\n[data-cm=\"fallback\"] {\n    background-color: blue;\n}\n```\n\nOf course you can style different Context Menus separately\n\n```CSS\n[data-cm=\"fallback\"], [data-cm=\"another-one\"] {\n    background-color: blue;\n}\n\n[data-cm=\"the-third-one\"] {\n    background-color: red;\n}\n```\n\nIn the example above the Context Menus with `name`s `\"fallback\"` and `\"another-one\"` would have blue background, whilst the Context Menu named `\"the-third-one\"` would have a red one.\n\nIf a Context Menu is unnamed then its `data-cm` attribute would be just an empty string, so in order to style such a menu you'd have to refer the `[data-cm]` in your CSS\n\n```CSS\n[data-cm] {\n    background-color: green;\n}\n```\n\nNow all the unnamed Context Menus will have green background.\n\nReferring empty `[data-cm]` attribute is also quite useful it you wish to style all the menus similarly except for a couple ones. For instance let's make all the Context Menus to have yellow backgrounds, while the `\"fallback\"` one would have a green one.\n\n```CSS\n[data-cm] {\n    background-color: yellow;\n}\n\n[data-cm=\"fallback\"] {\n    background-color: green;\n}\n```\n\nNow you can have hundreds of defferent named Context Menus styled the same and only the Context Menu with the `name` option set to `\"fallback\"` will have green background.\n\n**Notice**, that you must define styles for `[data-cm]` before you style any other named Context Menu. Otherwise the unnamed menu's styles will override the named one's.\n\n#### Structure\n\nWhen a Context Menu is opened it appears as the child of its overlay. Overlay is used as a sort of a grouping element and also to simplify the menu closure detection. The overlay is represented by a `\u003cdiv\u003e` element with `data-cm-overlay` attribute equal to the `name` of the menu and is spawned at the end of the `\u003cbody\u003e` whenever the menu is opened.\n\nThe menu itself consists of one element usually - the `\u003col\u003e`, which stores all the items. However 2 additional elements are added inside the `div[data-cm]` if the Context Menu is overflowed: the \"up\" and \"down\" arrow which represented by `\u003cdiv\u003e` elements with `data-cm-item-special=\"arrow up\"` and `data-cm-item-special=\"arrow down\"` attributes respectively.\n\nEach item of a Context Menu is represented by `\u003cli\u003e` with `data-cm-item` attribute the value of which equals the `title` of the item. Callers do also have `data-cm-item-caller` attribute assigned to them, and the special items get the `data-cm-item-special` equal the type of the special that is used (for example `data-cm-item-special=\"separator\"`).\n\nSo the stucture of a Context Menu might be represented as follows\n\n```HTML\n\u003cbody\u003e\n    \u003c!-- ... --\u003e\n    \u003cdiv data-cm-overlay=\"name\"\u003e\n        \u003cdiv data-cm=\"name\"\u003e\n            \u003c!-- \u003cdiv data-cm-item-special=\"arrow up\"\u003e\u003c/div\u003e - if the menu is overflowed --\u003e\n                \u003col\u003e\n                    \u003cli data-cm-item=\"Title 1\"\u003eTitle 1\u003c/li\u003e\n                    \u003cdiv data-cm-item-special=\"separator\"\u003e\u003c/div\u003e\n                    \u003cli data-cm-item=\"Title 2\"\u003eTitle 2\u003c/li\u003e\n                \u003c/ol\u003e\n            \u003c!-- \u003cdiv data-cm-item-special=\"arrow down\"\u003e\u003c/div\u003e - if the menu is overflowed --\u003e\n        \u003c/div\u003e\n    \u003c/div\u003e\n\u003c/body\u003e\n```\n\nAll the sub-menus of a Context Menu are stored in the same overlay as the \"root's\" one.\n\n#### Items\n\nWhen a mouse hovers an item, or a keyboard is used to navigate to the item then the item gets focused. So in order to highlight the item you must style its `:focuse` state\n\n```CSS\n[data-cm-item] {\n    background-color: white;\n}\n\n[data-cm-item]:focus {\n    background-color: purple;\n}\n```\n\nNow when a user navigates to a certain item the item would become highlighted purple. **Notice**, that browsers usually by default heighlight a focused element with `outline`, so if you wish to disable this behavior then add a `outline: none;` CSS property.\n\n#### Opening and closing transitions\n\nIn order to animate the Context Menu opening or closure use CSS transitions\n\n```CSS\n[data-cm] {\n    transition: opacity 1s linear;\n    opacity: 0;\n}\n\n[data-cm].visible {\n    transition: opacity 0.5s linear;\n    opacity: 1;\n}\n```\n\nIn the example above menus will smoothely appear in 1 second and hide in 0.5 seconds. The same way you can also animate overlays.\n\n**Very important**: use the same measurement units everywhere. Menus in the following example would behave **incorrect**\n\n```CSS\n[data-cm-overlay] {\n    transition: background-color 2s linear;\n    background-color: rgba(0, 0, 0, 0);\n}\n\n[data-cm-overlay].visible {\n    transition: background-color 900ms linear; // \"s\" is everywhere else, so don't use \"ms\" here (or instead use \"ms\" everywhere)\n    background-color: rgba(0, 0, 0, 0.5);\n}\n\n[data-cm] {\n    transition: opacity 1s linear;\n    opacity: 0;\n}\n\n[data-cm].visible {\n    transition: opacity 0.5s linear;\n    opacity: 1;\n}\n```\n\nDon't mix up seconds and milliseconds. Stick to one thing.\n\n## [Contribution](#contribution)\n\nI don't currently have any contribution manifest nor styleguides. Nevertheless, I'm open for any kind of contribution you can offer. So don't be shy to open an issue for everything you might want to know or let me know about or to make a pull request :sparkles:. Also, you can always contact me if you are unsure about what you can do to make this project better.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmellyshovel%2Fcustom-context-menu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmellyshovel%2Fcustom-context-menu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmellyshovel%2Fcustom-context-menu/lists"}