@alekstar79/context-menu 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 alekstar79
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # Context Menu TS (Vanilla/Vue3)
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/context-menu.svg)](https://www.npmjs.com/package/@alekstar79/context-menu)
4
+ [![License](https://img.shields.io/badge/License-MIT-blue)](LICENSE)
5
+ [![GitHub](https://img.shields.io/badge/github-repo-green.svg?style=flat)](https://github.com/alekstar79/context-menu)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat-square)](https://www.typescriptlang.org)
7
+ [![Coverage](https://img.shields.io/badge/coverage-81.65%25-brightgreen.svg)](https://github.com/alekstar79/comparison-slider)
8
+
9
+ > A beautiful and customizable radial context menu written in TypeScript.
10
+ > It can be used both as a plain JavaScript/TypeScript module and as a Vue component.
11
+ > The project includes full TypeScript support (types are exported) and provides a simple API for controlling the menu (show/hide, event subscription).
12
+ > The library is lightweight, has no required dependencies (Vue 3 is optional), and can be easily integrated into existing projects.
13
+
14
+ ![slider](menu.svg)
15
+
16
+ **[View Live Demo](https://alekstar79.github.io/context-menu)**
17
+
18
+ <!-- TOC -->
19
+ * [Context Menu TS (Vanilla/Vue3)](#context-menu-ts-vanillavue3)
20
+ * [✨ Features](#-features)
21
+ * [📦 Installation](#-installation)
22
+ * [🚀 Usage](#-usage)
23
+ * [⚙️ Configuration](#-configuration)
24
+ * [🧠 API](#-api)
25
+ * [Methods](#methods)
26
+ * [Events](#events)
27
+ * [Vue component CircularMenu](#vue-component-circularmenu)
28
+ * [💅 Styling](#-styling)
29
+ * [🛠️ Development](#-development)
30
+ * [Scripts](#scripts)
31
+ * [📄 License](#-license)
32
+ <!-- TOC -->
33
+
34
+ ## ✨ Features
35
+
36
+ 🎨 Customizable appearance (colors, radii, opacity)
37
+ 🖼️ SVG icon support via sprites
38
+ 💬 Hints with optional background
39
+ 🖱️ Hover and wheel animations
40
+ 🧩 Optional central button
41
+ 📦 Two usage modes: vanilla JS and Vue 3 component
42
+ ⚡ Full TypeScript support (types included)
43
+
44
+ ## 📦 Installation
45
+
46
+ ```bash
47
+ npm install context-menu
48
+ ```
49
+
50
+ The library has an optional peer dependency on vue. If you are using the Vue component, install Vue 3 as well:
51
+
52
+ ```bash
53
+ npm install vue@^3
54
+ ```
55
+
56
+ ## 🚀 Usage
57
+ 1. Vanilla JS / TypeScript
58
+
59
+ ```ts
60
+ import { defineConfig, Manager } from 'context-menu-ts'
61
+
62
+ const config = defineConfig({
63
+ sprite: '/path/to/icons.svg', // path to SVG sprite
64
+ innerRadius: 50,
65
+ outerRadius: 150,
66
+ opacity: 0.7,
67
+ color: '#1976D2',
68
+ sectors: [
69
+ { icon: 'new', hint: 'New' },
70
+ { icon: 'open', hint: 'Open' },
71
+ { icon: 'save', hint: 'Save' }
72
+ ],
73
+ centralButton: {
74
+ icon: 'home',
75
+ hint: 'Home',
76
+ hintPosition: 'top'
77
+ }
78
+ })
79
+
80
+ // container element where the menu will be placed
81
+ const container = document.getElementById('menu-container')
82
+ const menu = new Manager(container, config)
83
+
84
+ // subscribe to events
85
+ menu.on('click', (data) => {
86
+ console.log(`Selected: ${data.hint}`)
87
+ })
88
+
89
+ // show menu at cursor position (e.g., on contextmenu)
90
+ window.addEventListener('contextmenu', (e) => {
91
+ e.preventDefault()
92
+ menu.show(e)
93
+ })
94
+ ```
95
+
96
+ 2. Vue Component
97
+
98
+ ```vue
99
+ <template>
100
+ <CircularMenu
101
+ ref="menuRef"
102
+ :sprite="sprite"
103
+ :inner-radius="75"
104
+ :outer-radius="150"
105
+ :sectors="sectors"
106
+ :central-button="centralButton"
107
+ color="#42B883"
108
+ @click="onMenuItemClick"
109
+ />
110
+ </template>
111
+
112
+ <script setup lang="ts">
113
+ import { ref } from 'vue'
114
+ import ContextMenu from 'context-menu/vue'
115
+ import type { ISector } from 'context-menu'
116
+
117
+ const sprite = '/icons.svg'
118
+ const sectors: ISector[] = [
119
+ { icon: 'new', hint: 'New' },
120
+ { icon: 'open', hint: 'Open' },
121
+ { icon: 'save', hint: 'Save' }
122
+ ];
123
+
124
+ const centralButton = {
125
+ icon: 'home',
126
+ hint: 'Home',
127
+ hintPosition: 'bottom'
128
+ }
129
+
130
+ const menuRef = ref()
131
+
132
+ const onMenuItemClick = (data: { icon: string; hint: string }) => {
133
+ console.log('Selected:', data.hint)
134
+ }
135
+
136
+ // Example of manually showing the menu
137
+ const showMenu = (e: PointerEvent) => {
138
+ menuRef.value?.show(e)
139
+ }
140
+ </script>
141
+ ```
142
+
143
+ ## ⚙️ Configuration
144
+ `defineConfig(options: Partial<IConfig>): IConfig`
145
+
146
+ Creates a configuration object with default values.
147
+
148
+ `IConfig` interface
149
+
150
+ | Property | Type | Default | Description |
151
+ |---------------------|----------------|----------------|----------------------------------------------------------------------------------------------|
152
+ | sprite | string | `../icons.svg` | Path to the SVG sprite containing icons |
153
+ | innerRadius | number | – | Inner radius of sectors (the smaller ring radius) |
154
+ | outerRadius | number | – | Outer radius of sectors |
155
+ | opacity | number | `0.7` | Opacity of sectors and hint backgrounds (if any) |
156
+ | color | string | – | Main color for sectors and hint backgrounds (default '#1976D2' if omitted) |
157
+ | hintPadding | number | – | Padding around the hint text (pixels). If not set, the hint is rendered without a background |
158
+ | iconScale | number | – | Global scale factor for all icons (can be overridden per sector) |
159
+ | iconRadius | number | – | Global radius at which icons are placed (can be overridden per sector) |
160
+ | sectors | ISector[] | `[]` | Array of menu sectors |
161
+ | centralButton | ICentralButton | – | Configuration for the central button |
162
+ | autoBindContextMenu | boolean | `true` | Automatically bind the contextmenu event listener to window |
163
+
164
+
165
+ `ISector` interface
166
+
167
+ | Property | Type | Default | Description |
168
+ |-------------|------------|---------|-------------------------------------------|
169
+ | icon | string | – | Icon identifier (without #) |
170
+ | hint | string | – | Hint text |
171
+ | onclick | () => void | – | Click handler for the sector |
172
+ | rotate | number | `0` | Additional rotation of the icon (degrees) |
173
+ | iconScale | number | global | Icon scale for this specific sector |
174
+ | iconRadius | number | global | Icon placement radius for this sector |
175
+ | hintPadding | number | global | Hint padding for this sector |
176
+
177
+
178
+ `ICentralButton` interface
179
+
180
+ | Property | Type | Default | Description |
181
+ |----------------|------------------|---------|-----------------------------------------------------------------|
182
+ | icon | string | – | Icon identifier |
183
+ | hint | string | – | Hint text |
184
+ | onclick | () => void | – | Click handler |
185
+ | iconScale | number | global | Icon scale |
186
+ | iconRadius | number | global | Icon radius (effectively the button size) |
187
+ | hintPosition | `top \| bottom` | `top` | Position of the hint relative to the button |
188
+ | hintSpan | number | `180` | Angular span of the hint arc (degrees) |
189
+ | hintDistance | number | `8` | Distance from button edge to the hint text (when no background) |
190
+ | hintOffset | number | – | Absolute offset of the hint (overrides hintDistance) |
191
+ | hintPadding | number | global | Hint padding for the central button |
192
+ | hintStartAngle | number | – | Start angle of the arc (must be set together with hintEndAngle) |
193
+ | hintEndAngle | number | – | End angle of the arc |
194
+
195
+
196
+ ## 🧠 API
197
+
198
+ `Manager` class (vanilla)
199
+
200
+ ```ts
201
+ const menu = new Manager(container: HTMLElement, config: IConfig)
202
+ ```
203
+
204
+ ### Methods
205
+
206
+ - `show(event: MouseEvent | PointerEvent)` – Shows the menu at the cursor position.
207
+ - `hide()` – Hides the menu.
208
+ - `on(event: string, callback: Function)` – Subscribes to events.
209
+
210
+ ### Events
211
+
212
+ - `click` – Emitted when a sector or the central button is clicked. The callback receives `{ icon: string; hint: string }`.
213
+
214
+ ### Vue component CircularMenu
215
+
216
+ **Props**
217
+
218
+ All properties from `IConfig` except `sectors` and `centralButton` (they are passed separately).
219
+ Additional props:
220
+
221
+ - `autoBindContextMenu` – Automatically bind contextmenu to window (default `true`).
222
+
223
+ **Events**
224
+
225
+ - `click` – Same as the vanilla `click` event.
226
+
227
+ **Exposed methods (via `ref`)**
228
+
229
+ - `show(event: PointerEvent)` – Shows the menu.
230
+ - `hide()` – Hides the menu.
231
+
232
+ ## 💅 Styling
233
+
234
+ > The library comes with basic styles that are automatically included. You can override them using the following CSS classes:
235
+
236
+ - `.context` – Root menu container.
237
+ - `.radial-menu-svg` – The SVG element.
238
+ - `.radial-sector` – A sector.
239
+ - `.radial-icon` – An icon.
240
+ - `.radial-hint` – Hint text.
241
+ - `.radial-hint-bg` – Hint background.
242
+ - `.central-sector`, `.central-icon`, `.central-hint` – Central button elements.
243
+ - `.active` – Class added to hints on hover.
244
+
245
+ Example of customization:
246
+
247
+ ```scss
248
+ .radial-hint {
249
+ font-size: 14px;
250
+ font-weight: 600;
251
+ fill: #000;
252
+ }
253
+ ```
254
+
255
+ ## 🛠️ Development
256
+
257
+ ```bash
258
+ git clone <repo>
259
+ cd context-menu
260
+ npm install
261
+ ```
262
+
263
+ ### Scripts
264
+
265
+ - `npm run dev` – Start dev server with demo.
266
+ - `npm run build` – Build the library.
267
+ - `npm run build:demo` – Build the demo app.
268
+ - `npm run test` – Run tests.
269
+ - `npm run test:ui` – Run tests with UI.
270
+ - `npm run test:coverage` – Run tests with coverage.
271
+
272
+ Project structure:
273
+
274
+ - `src/` – Source code.
275
+ - `src/menu/` – Core menu implementation.
276
+ - `src/components/` – Vue component.
277
+ - `src/core/` – Utilities and SVG wrapper.
278
+ - `public/` – Static assets for the demo.
279
+
280
+ ## 📄 License
281
+
282
+ MIT © [alekstar79](https://github.com/alekstar79)
283
+
284
+ For more examples and a live demo, visit the repository. Feel free to open issues for questions or suggestions.
@@ -0,0 +1,31 @@
1
+ import { IConfig, ISector } from '../menu/config';
2
+ interface Props {
3
+ sprite: string;
4
+ innerRadius: number;
5
+ outerRadius: number;
6
+ opacity?: number;
7
+ iconScale?: number;
8
+ iconRadius?: number;
9
+ sectors: ISector[];
10
+ color?: string;
11
+ hintPadding?: number;
12
+ centralButton?: IConfig['centralButton'];
13
+ autoBindContextMenu?: boolean;
14
+ }
15
+ declare const _default: import('vue').DefineComponent<Props, {
16
+ show: (event: PointerEvent) => void | undefined;
17
+ hide: () => void | undefined;
18
+ }, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
19
+ click: (data: {
20
+ icon: string;
21
+ hint: string;
22
+ }) => any;
23
+ }, string, import('vue').PublicProps, Readonly<Props> & Readonly<{
24
+ onClick?: ((data: {
25
+ icon: string;
26
+ hint: string;
27
+ }) => any) | undefined;
28
+ }>, {
29
+ autoBindContextMenu: boolean;
30
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
31
+ export default _default;
@@ -0,0 +1,2 @@
1
+ export * from './vue'
2
+ export {}
@@ -0,0 +1,74 @@
1
+ import { Manager as m } from "./context-menu.js";
2
+ import { defineConfig as y } from "./context-menu.js";
3
+ import { defineComponent as f, onMounted as h, onUnmounted as C, watch as R, getCurrentInstance as x } from "vue";
4
+ const B = /* @__PURE__ */ f({
5
+ __name: "ContextMenu",
6
+ props: {
7
+ sprite: {},
8
+ innerRadius: {},
9
+ outerRadius: {},
10
+ opacity: {},
11
+ iconScale: {},
12
+ iconRadius: {},
13
+ sectors: {},
14
+ color: {},
15
+ hintPadding: {},
16
+ centralButton: {},
17
+ autoBindContextMenu: { type: Boolean, default: !0 }
18
+ },
19
+ emits: ["click"],
20
+ setup(s, { expose: d, emit: l }) {
21
+ const e = s, p = l;
22
+ let i = null, t = null;
23
+ function c() {
24
+ const n = {
25
+ autoBindContextMenu: e.autoBindContextMenu,
26
+ sprite: e.sprite,
27
+ innerRadius: e.innerRadius,
28
+ outerRadius: e.outerRadius,
29
+ opacity: e.opacity ?? 0.7,
30
+ iconScale: e.iconScale,
31
+ iconRadius: e.iconRadius,
32
+ sectors: e.sectors,
33
+ centralButton: e.centralButton,
34
+ color: e.color,
35
+ hintPadding: e.hintPadding
36
+ }, o = document.createElement("div");
37
+ return i = new m(o, n), i.on("click", (r) => p("click", r)), t = o.firstChild, o.removeChild(t), t;
38
+ }
39
+ function a(n) {
40
+ const o = x(), r = o?.vnode.el, u = r?.parentNode;
41
+ u && r && n && (u.replaceChild(n, r), o.vnode.el = n);
42
+ }
43
+ return h(() => {
44
+ a(c());
45
+ }), C(() => {
46
+ t && t.parentNode && t.parentNode.removeChild(t), i = null;
47
+ }), R(() => [
48
+ e.sprite,
49
+ e.innerRadius,
50
+ e.outerRadius,
51
+ e.opacity,
52
+ e.iconScale,
53
+ e.iconRadius,
54
+ e.sectors,
55
+ e.centralButton,
56
+ e.color,
57
+ e.hintPadding,
58
+ e.autoBindContextMenu
59
+ ], () => {
60
+ if (!t) return;
61
+ const n = c();
62
+ a(n), t = n;
63
+ }, { deep: !0 }), d({
64
+ show: (n) => i?.show(n),
65
+ hide: () => i?.hide()
66
+ }), (n, o) => null;
67
+ }
68
+ });
69
+ export {
70
+ B as ContextMenu,
71
+ m as Manager,
72
+ y as defineConfig
73
+ };
74
+ //# sourceMappingURL=context-menu-vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-menu-vue.js","sources":["../src/components/ContextMenu.vue"],"sourcesContent":["<template></template>\n\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, watch, getCurrentInstance } from 'vue'\nimport type { IConfig, ISector } from '@/menu/config'\nimport { Manager } from '@/menu/manager'\n\ninterface Props {\n sprite: string;\n innerRadius: number;\n outerRadius: number;\n opacity?: number;\n iconScale?: number;\n iconRadius?: number;\n sectors: ISector[];\n color?: string;\n hintPadding?: number;\n centralButton?: IConfig['centralButton'];\n autoBindContextMenu?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n autoBindContextMenu: true\n})\n\nconst emit = defineEmits<{\n (e: 'click', data: { icon: string; hint: string }): void;\n}>();\n\nlet menuManager: Manager | null = null\nlet menuElement: HTMLElement | null = null\n\nfunction createMenu() {\n const config: IConfig = {\n autoBindContextMenu: props.autoBindContextMenu,\n sprite: props.sprite,\n innerRadius: props.innerRadius,\n outerRadius: props.outerRadius,\n opacity: props.opacity ?? 0.7,\n iconScale: props.iconScale,\n iconRadius: props.iconRadius,\n sectors: props.sectors,\n centralButton: props.centralButton,\n color: props.color,\n hintPadding: props.hintPadding,\n }\n\n const tempContainer = document.createElement('div')\n\n menuManager = new Manager(tempContainer, config)\n menuManager.on('click', (data: any) => emit('click', data))\n menuElement = tempContainer.firstChild as HTMLElement\n tempContainer.removeChild(menuElement)\n\n return menuElement\n}\n\nfunction replaceRoot(newElement: HTMLElement) {\n const instance = getCurrentInstance()\n const oldEl = instance?.vnode.el\n const parent = oldEl?.parentNode\n\n if (parent && oldEl && newElement) {\n parent.replaceChild(newElement, oldEl)\n instance.vnode.el = newElement\n }\n}\n\nonMounted(() => {\n replaceRoot(createMenu())\n})\n\nonUnmounted(() => {\n if (menuElement && menuElement.parentNode) {\n menuElement.parentNode.removeChild(menuElement)\n }\n\n menuManager = null\n})\n\nwatch(() => [\n props.sprite,\n props.innerRadius,\n props.outerRadius,\n props.opacity,\n props.iconScale,\n props.iconRadius,\n props.sectors,\n props.centralButton,\n props.color,\n props.hintPadding,\n props.autoBindContextMenu,\n], () => {\n if (!menuElement) return\n\n const newEl = createMenu()\n replaceRoot(newEl)\n menuElement = newEl\n}, { deep: true })\n\ndefineExpose({\n show: (event: PointerEvent) => menuManager?.show(event),\n hide: () => menuManager?.hide()\n})\n</script>\n"],"names":["props","__props","emit","__emit","menuManager","menuElement","createMenu","config","tempContainer","Manager","data","replaceRoot","newElement","instance","getCurrentInstance","oldEl","parent","onMounted","onUnmounted","watch","newEl","__expose","event"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,UAAMA,IAAQC,GAIRC,IAAOC;AAIb,QAAIC,IAA8B,MAC9BC,IAAkC;AAEtC,aAASC,IAAa;AACpB,YAAMC,IAAkB;AAAA,QACtB,qBAAqBP,EAAM;AAAA,QAC3B,QAAQA,EAAM;AAAA,QACd,aAAaA,EAAM;AAAA,QACnB,aAAaA,EAAM;AAAA,QACnB,SAASA,EAAM,WAAW;AAAA,QAC1B,WAAWA,EAAM;AAAA,QACjB,YAAYA,EAAM;AAAA,QAClB,SAASA,EAAM;AAAA,QACf,eAAeA,EAAM;AAAA,QACrB,OAAOA,EAAM;AAAA,QACb,aAAaA,EAAM;AAAA,MAAA,GAGfQ,IAAgB,SAAS,cAAc,KAAK;AAElD,aAAAJ,IAAc,IAAIK,EAAQD,GAAeD,CAAM,GAC/CH,EAAY,GAAG,SAAS,CAACM,MAAcR,EAAK,SAASQ,CAAI,CAAC,GAC1DL,IAAcG,EAAc,YAC5BA,EAAc,YAAYH,CAAW,GAE9BA;AAAA,IACT;AAEA,aAASM,EAAYC,GAAyB;AAC5C,YAAMC,IAAWC,EAAA,GACXC,IAAQF,GAAU,MAAM,IACxBG,IAASD,GAAO;AAEtB,MAAIC,KAAUD,KAASH,MACrBI,EAAO,aAAaJ,GAAYG,CAAK,GACrCF,EAAS,MAAM,KAAKD;AAAA,IAExB;AAEA,WAAAK,EAAU,MAAM;AACd,MAAAN,EAAYL,GAAY;AAAA,IAC1B,CAAC,GAEDY,EAAY,MAAM;AAChB,MAAIb,KAAeA,EAAY,cAC7BA,EAAY,WAAW,YAAYA,CAAW,GAGhDD,IAAc;AAAA,IAChB,CAAC,GAEDe,EAAM,MAAM;AAAA,MACVnB,EAAM;AAAA,MACNA,EAAM;AAAA,MACNA,EAAM;AAAA,MACNA,EAAM;AAAA,MACNA,EAAM;AAAA,MACNA,EAAM;AAAA,MACNA,EAAM;AAAA,MACNA,EAAM;AAAA,MACNA,EAAM;AAAA,MACNA,EAAM;AAAA,MACNA,EAAM;AAAA,IAAA,GACL,MAAM;AACP,UAAI,CAACK,EAAa;AAElB,YAAMe,IAAQd,EAAA;AACd,MAAAK,EAAYS,CAAK,GACjBf,IAAce;AAAA,IAChB,GAAG,EAAE,MAAM,IAAM,GAEjBC,EAAa;AAAA,MACX,MAAM,CAACC,MAAwBlB,GAAa,KAAKkB,CAAK;AAAA,MACtD,MAAM,MAAMlB,GAAa,KAAA;AAAA,IAAK,CAC/B;;;"}
@@ -0,0 +1,2 @@
1
+ export * from './index'
2
+ export {}