@enegalan/contextmenu.js 1.4.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 +21 -0
- package/README.md +312 -0
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +483 -0
- package/dist/index.d.ts +483 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/style.css +1 -0
- package/package.json +46 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual variant for menu items (action, submenu, checkbox, radio).
|
|
3
|
+
*/
|
|
4
|
+
type MenuItemVariant = "default" | "danger" | "info" | "success" | "warning" | "muted";
|
|
5
|
+
/**
|
|
6
|
+
* Badge shown to the right of the label/shortcut. Simple (string/number) or fully customizable.
|
|
7
|
+
* With object: use `content` + `className`, or `render()` for a custom element.
|
|
8
|
+
*/
|
|
9
|
+
type BadgeConfig = string | number | {
|
|
10
|
+
/** Text or number to display. Ignored if `render` is provided. */
|
|
11
|
+
content?: string | number;
|
|
12
|
+
/** CSS class(es) on the badge element (in addition to the default .cm-item-badge when not using render). */
|
|
13
|
+
className?: string;
|
|
14
|
+
/** Return a custom element for full control. Element is appended as-is; set aria-hidden="true" if decorative. */
|
|
15
|
+
render?: () => HTMLElement;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Config for the loading spinner. Can be set globally (config.spinner) or per item (loadingIcon, loadingSize, loadingSpeed).
|
|
19
|
+
*/
|
|
20
|
+
interface SpinnerConfig {
|
|
21
|
+
/** Custom spinner: SVG string or HTMLElement. Omit to use the default CSS circle. */
|
|
22
|
+
icon?: string | HTMLElement;
|
|
23
|
+
/** Size in px (number) or CSS length (e.g. "0.75rem"). */
|
|
24
|
+
size?: number | string;
|
|
25
|
+
/** Duration of one full rotation in ms. Default 600. */
|
|
26
|
+
speed?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Optional event listener or listener with EventListenerOptions for a menu item.
|
|
30
|
+
*/
|
|
31
|
+
type EventRegistryEntry<K extends keyof HTMLElementEventMap = keyof HTMLElementEventMap> = ((e: HTMLElementEventMap[K]) => void) | {
|
|
32
|
+
listener: (e: HTMLElementEventMap[K]) => void;
|
|
33
|
+
options?: EventListenerOptions;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Map of DOM event names to handlers. Each value is either a listener or { listener, options? }.
|
|
37
|
+
*/
|
|
38
|
+
type EventRegistry = {
|
|
39
|
+
[K in keyof HTMLElementEventMap]?: EventRegistryEntry<K>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Base properties for all menu items.
|
|
43
|
+
*/
|
|
44
|
+
type MenuItemBase = {
|
|
45
|
+
/** The ID of the item. */
|
|
46
|
+
id?: string;
|
|
47
|
+
/** Whether the item is disabled. */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
/** Whether the item is visible. */
|
|
50
|
+
visible?: boolean;
|
|
51
|
+
/** When true, show loading state and block interaction. */
|
|
52
|
+
loading?: boolean;
|
|
53
|
+
/** Custom loading spinner icon (SVG string or HTMLElement). Overrides config.spinner.icon. */
|
|
54
|
+
loadingIcon?: string | HTMLElement;
|
|
55
|
+
/** Loading spinner size in px or CSS length. Overrides config.spinner.size. */
|
|
56
|
+
loadingSize?: number | string;
|
|
57
|
+
/** Loading spinner rotation speed: ms per full turn. Overrides config.spinner.speed. */
|
|
58
|
+
loadingSpeed?: number;
|
|
59
|
+
/** Visual variant; adds class cm-item--{variant}. */
|
|
60
|
+
variant?: MenuItemVariant;
|
|
61
|
+
/** CSS class(es) on the item. */
|
|
62
|
+
className?: string;
|
|
63
|
+
/** Optional custom DOM event handlers on the item element. */
|
|
64
|
+
events?: EventRegistry | (() => EventRegistry);
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Menu item action.
|
|
68
|
+
*/
|
|
69
|
+
type MenuItemAction = MenuItemBase & {
|
|
70
|
+
/** The type of the action. */
|
|
71
|
+
type: "item";
|
|
72
|
+
/** The label of the action. */
|
|
73
|
+
label: string;
|
|
74
|
+
/** Optional icon in the action. */
|
|
75
|
+
icon?: string | HTMLElement;
|
|
76
|
+
/** Shortcut key for the action. */
|
|
77
|
+
shortcut?: string;
|
|
78
|
+
/** Optional badge shown to the right of the label/shortcut. String/number or BadgeConfig (content, className, render). */
|
|
79
|
+
badge?: BadgeConfig;
|
|
80
|
+
/** The function to call when the action is clicked. */
|
|
81
|
+
onClick?: (event: MenuClickEvent) => void;
|
|
82
|
+
/** When false, keep the menu open after click. Default is true (close). */
|
|
83
|
+
closeOnAction?: boolean;
|
|
84
|
+
/** Return a custom element for full control over appearance. Library still sets role, tabindex, and wires click. */
|
|
85
|
+
render?: (item: MenuItemAction) => HTMLElement;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Where to open the submenu relative to the parent.
|
|
89
|
+
*/
|
|
90
|
+
type SubmenuPlacement = "right" | "left" | "auto";
|
|
91
|
+
/**
|
|
92
|
+
* Submenu children: array, or sync/async function that returns items (resolved when submenu opens).
|
|
93
|
+
*/
|
|
94
|
+
type SubmenuChildren = MenuItem[] | (() => MenuItem[]) | (() => Promise<MenuItem[]>);
|
|
95
|
+
/**
|
|
96
|
+
* Submenu item.
|
|
97
|
+
*/
|
|
98
|
+
type MenuItemSubmenu = MenuItemBase & {
|
|
99
|
+
/** The type of the submenu. */
|
|
100
|
+
type: "submenu";
|
|
101
|
+
/** The label of the submenu. */
|
|
102
|
+
label: string;
|
|
103
|
+
/** Optional icon in the submenu. */
|
|
104
|
+
icon?: string | HTMLElement;
|
|
105
|
+
/** Shortcut key for the submenu. */
|
|
106
|
+
shortcut?: string;
|
|
107
|
+
/** Optional badge shown to the right of the label/shortcut. String/number or BadgeConfig (content, className, render). */
|
|
108
|
+
badge?: BadgeConfig;
|
|
109
|
+
/** The children of the submenu. Array or function returning items (or Promise); resolved when submenu opens. */
|
|
110
|
+
children: SubmenuChildren;
|
|
111
|
+
/** Where to open the submenu relative to the parent. Overrides config.submenuPlacement. */
|
|
112
|
+
submenuPlacement?: SubmenuPlacement;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Menu item separator.
|
|
116
|
+
*/
|
|
117
|
+
type MenuItemSeparator = {
|
|
118
|
+
/** The type of the separator. */
|
|
119
|
+
type: "separator";
|
|
120
|
+
/** The ID of the separator. */
|
|
121
|
+
id?: string;
|
|
122
|
+
/** CSS class(es) on the separator. */
|
|
123
|
+
className?: string;
|
|
124
|
+
/** Optional custom DOM event handlers on the separator element. */
|
|
125
|
+
events?: EventRegistry | (() => EventRegistry);
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Menu item checkbox.
|
|
129
|
+
*/
|
|
130
|
+
type MenuItemCheckbox = MenuItemBase & {
|
|
131
|
+
/** The type of the checkbox. */
|
|
132
|
+
type: "checkbox";
|
|
133
|
+
/** The label of the checkbox. */
|
|
134
|
+
label: string;
|
|
135
|
+
/** Optional icon in the row. */
|
|
136
|
+
leadingIcon?: string | HTMLElement;
|
|
137
|
+
/** Shortcut key for the checkbox. */
|
|
138
|
+
shortcut?: string;
|
|
139
|
+
/** Whether the checkbox is checked. */
|
|
140
|
+
checked?: boolean;
|
|
141
|
+
/** Custom indicator when checked. */
|
|
142
|
+
icon?: string | HTMLElement;
|
|
143
|
+
/** Custom indicator when unchecked. If omitted, the indicator area is empty when unchecked. */
|
|
144
|
+
uncheckedIcon?: string | HTMLElement;
|
|
145
|
+
/** CSS class(es) on the indicator wrapper when checked. */
|
|
146
|
+
checkedClassName?: string;
|
|
147
|
+
/** CSS class(es) on the indicator wrapper when unchecked. */
|
|
148
|
+
uncheckedClassName?: string;
|
|
149
|
+
/** The function to call when the checkbox is changed. */
|
|
150
|
+
onChange?: (event: MenuCheckboxChangeEvent) => void;
|
|
151
|
+
/** When false, keep the menu open after change. Default is true (close). */
|
|
152
|
+
closeOnAction?: boolean;
|
|
153
|
+
/** Return a custom element for full control over appearance. Library still sets role, aria-checked, tabindex, and wires click. */
|
|
154
|
+
render?: (item: MenuItemCheckbox) => HTMLElement;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Menu item radio.
|
|
158
|
+
*/
|
|
159
|
+
type MenuItemRadio = MenuItemBase & {
|
|
160
|
+
/** The type of the radio button. */
|
|
161
|
+
type: "radio";
|
|
162
|
+
/** The label of the radio button. */
|
|
163
|
+
label: string;
|
|
164
|
+
/** The name of the radio button. */
|
|
165
|
+
name: string;
|
|
166
|
+
/** The value of the radio button. */
|
|
167
|
+
value: string;
|
|
168
|
+
/** Optional icon in the row. */
|
|
169
|
+
leadingIcon?: string | HTMLElement;
|
|
170
|
+
/** Shortcut key for the radio button. */
|
|
171
|
+
shortcut?: string;
|
|
172
|
+
/** Whether the radio button is checked. */
|
|
173
|
+
checked?: boolean;
|
|
174
|
+
/** Custom indicator when selected. */
|
|
175
|
+
icon?: string | HTMLElement;
|
|
176
|
+
/** Custom indicator when not selected. If omitted, the indicator area is empty when unselected. */
|
|
177
|
+
uncheckedIcon?: string | HTMLElement;
|
|
178
|
+
/** CSS class(es) on the indicator wrapper when selected. */
|
|
179
|
+
checkedClassName?: string;
|
|
180
|
+
/** CSS class(es) on the indicator wrapper when not selected. */
|
|
181
|
+
uncheckedClassName?: string;
|
|
182
|
+
/** The function to call when the radio button is selected. */
|
|
183
|
+
onSelect?: (event: MenuRadioSelectEvent) => void;
|
|
184
|
+
/** When false, keep the menu open after select. Default is true (close). */
|
|
185
|
+
closeOnAction?: boolean;
|
|
186
|
+
/** Return a custom element for full control over appearance. Library still sets role, aria-checked, tabindex, and wires click. */
|
|
187
|
+
render?: (item: MenuItemRadio) => HTMLElement;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Menu item label.
|
|
191
|
+
*/
|
|
192
|
+
type MenuItemLabel = {
|
|
193
|
+
/** The type of the label. */
|
|
194
|
+
type: "label";
|
|
195
|
+
/** The label of the label. */
|
|
196
|
+
label: string;
|
|
197
|
+
/** The ID of the label. */
|
|
198
|
+
id?: string;
|
|
199
|
+
/** CSS class(es) on the label. */
|
|
200
|
+
className?: string;
|
|
201
|
+
/** Optional custom DOM event handlers on the label element. */
|
|
202
|
+
events?: EventRegistry | (() => EventRegistry);
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Menu item link.
|
|
206
|
+
*/
|
|
207
|
+
type MenuItemLink = MenuItemBase & {
|
|
208
|
+
/** The type of the link. */
|
|
209
|
+
type: "link";
|
|
210
|
+
/** The label of the link. */
|
|
211
|
+
label: string;
|
|
212
|
+
/** The URL to navigate to. */
|
|
213
|
+
href: string;
|
|
214
|
+
/** Optional icon in the link. */
|
|
215
|
+
icon?: string | HTMLElement;
|
|
216
|
+
/** Shortcut key for the link. */
|
|
217
|
+
shortcut?: string;
|
|
218
|
+
/** Optional badge shown to the right of the label/shortcut. String/number or BadgeConfig (content, className, render). */
|
|
219
|
+
badge?: BadgeConfig;
|
|
220
|
+
/** Link target, e.g. "_blank". */
|
|
221
|
+
target?: string;
|
|
222
|
+
/** Link rel, e.g. "noopener". */
|
|
223
|
+
rel?: string;
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Menu item.
|
|
227
|
+
*/
|
|
228
|
+
type MenuItem = MenuItemAction | MenuItemSubmenu | MenuItemSeparator | MenuItemCheckbox | MenuItemRadio | MenuItemLabel | MenuItemLink;
|
|
229
|
+
/**
|
|
230
|
+
* Menu click event.
|
|
231
|
+
*/
|
|
232
|
+
interface MenuClickEvent {
|
|
233
|
+
/** The item that was clicked. */
|
|
234
|
+
item: MenuItemAction;
|
|
235
|
+
/** The native event that caused the click. */
|
|
236
|
+
nativeEvent: MouseEvent | KeyboardEvent;
|
|
237
|
+
/** The function to close the menu. */
|
|
238
|
+
close: () => void;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Menu checkbox change event.
|
|
242
|
+
*/
|
|
243
|
+
interface MenuCheckboxChangeEvent {
|
|
244
|
+
/** The checkbox that was changed. */
|
|
245
|
+
item: MenuItemCheckbox;
|
|
246
|
+
/** Whether the checkbox is checked. */
|
|
247
|
+
checked: boolean;
|
|
248
|
+
/** The native event that caused the change. */
|
|
249
|
+
nativeEvent: MouseEvent | KeyboardEvent;
|
|
250
|
+
/** The function to close the menu. */
|
|
251
|
+
close: () => void;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Menu radio select event.
|
|
255
|
+
*/
|
|
256
|
+
interface MenuRadioSelectEvent {
|
|
257
|
+
/** The radio button that was selected. */
|
|
258
|
+
item: MenuItemRadio;
|
|
259
|
+
/** The value of the radio button. */
|
|
260
|
+
value: string;
|
|
261
|
+
/** The native event that caused the selection. */
|
|
262
|
+
nativeEvent: MouseEvent | KeyboardEvent;
|
|
263
|
+
/** The function to close the menu. */
|
|
264
|
+
close: () => void;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Theme configuration.
|
|
268
|
+
*/
|
|
269
|
+
interface ThemeConfig {
|
|
270
|
+
/** The CSS class(es) to apply to the menu. */
|
|
271
|
+
class?: string;
|
|
272
|
+
/** The tokens to apply to the menu. */
|
|
273
|
+
tokens?: Record<string, string>;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Animation type.
|
|
277
|
+
*/
|
|
278
|
+
type AnimationType = "fade" | "slide";
|
|
279
|
+
/**
|
|
280
|
+
* Animation configuration.
|
|
281
|
+
*/
|
|
282
|
+
interface AnimationConfig {
|
|
283
|
+
/** Animation style: "fade" (opacity + scale) or "slide" (opacity + translate). Default "fade". */
|
|
284
|
+
type?: AnimationType;
|
|
285
|
+
/** The duration of the enter animation. */
|
|
286
|
+
enter?: number | {
|
|
287
|
+
duration: number;
|
|
288
|
+
easing: string;
|
|
289
|
+
};
|
|
290
|
+
/** The duration of the leave animation. */
|
|
291
|
+
leave?: number | {
|
|
292
|
+
duration: number;
|
|
293
|
+
easing: string;
|
|
294
|
+
};
|
|
295
|
+
/** Whether the animations are disabled. */
|
|
296
|
+
disabled?: boolean;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Position configuration.
|
|
300
|
+
*/
|
|
301
|
+
interface PositionConfig {
|
|
302
|
+
/** The offset from the anchor. */
|
|
303
|
+
offset?: {
|
|
304
|
+
x: number;
|
|
305
|
+
y: number;
|
|
306
|
+
};
|
|
307
|
+
/** The padding around the menu. */
|
|
308
|
+
padding?: number;
|
|
309
|
+
/** Whether to flip the menu to keep it in view. */
|
|
310
|
+
flip?: boolean;
|
|
311
|
+
/** Whether to shift the menu to keep it in view. */
|
|
312
|
+
shift?: boolean;
|
|
313
|
+
/** Base z-index for the root menu. Used with submenuZIndexStep for stacking. */
|
|
314
|
+
zIndexBase?: number;
|
|
315
|
+
/** Z-index increment per submenu level so each submenu stacks above the previous. 0 = no increment. */
|
|
316
|
+
submenuZIndexStep?: number;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Config for the arrow shown on parent items that have a submenu.
|
|
320
|
+
* Set to `true` to use the default arrow with no options.
|
|
321
|
+
*/
|
|
322
|
+
interface SubmenuArrowConfig {
|
|
323
|
+
/** Custom icon: SVG string or HTMLElement. When submenuArrow is true the default is a chevron SVG; omit icon in an object to use the CSS triangle. */
|
|
324
|
+
icon?: string | HTMLElement;
|
|
325
|
+
/** Size in px (number) or CSS length (e.g. "0.5rem"). For default arrow, scales the triangle; for custom icon, sets width and height. */
|
|
326
|
+
size?: number | string;
|
|
327
|
+
/** Extra class name(s) on the arrow wrapper. */
|
|
328
|
+
className?: string;
|
|
329
|
+
/** Opacity 0–1. */
|
|
330
|
+
opacity?: number;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Placement for opening the menu at an element.
|
|
334
|
+
*/
|
|
335
|
+
type OpenAtElementPlacement = "bottom-start" | "bottom-end" | "top-start" | "top-end" | "left-start" | "left-end" | "right-start" | "right-end" | "auto";
|
|
336
|
+
/**
|
|
337
|
+
* Options for opening the menu at an element.
|
|
338
|
+
*/
|
|
339
|
+
type OpenAtElementOptions = {
|
|
340
|
+
/** The placement of the menu; "auto" picks the best side based on viewport space. */
|
|
341
|
+
placement?: OpenAtElementPlacement;
|
|
342
|
+
/** The offset from the anchor. */
|
|
343
|
+
offset?: {
|
|
344
|
+
x: number;
|
|
345
|
+
y: number;
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
/**
|
|
349
|
+
* Element to bind so the menu opens on contextmenu (desktop) and long-press (touch).
|
|
350
|
+
* Either pass the element directly or { element, options } for bind options (e.g. longPressMs).
|
|
351
|
+
*/
|
|
352
|
+
type ContextMenuBindConfig = HTMLElement | {
|
|
353
|
+
element: HTMLElement;
|
|
354
|
+
options?: BindOptions;
|
|
355
|
+
};
|
|
356
|
+
/** Context passed to onBeforeOpen (and optionally to other open hooks). */
|
|
357
|
+
interface OpenContext {
|
|
358
|
+
/** X coordinate where the menu will open. */
|
|
359
|
+
x: number;
|
|
360
|
+
/** Y coordinate where the menu will open. */
|
|
361
|
+
y: number;
|
|
362
|
+
/** Element that received the context menu event, if any. */
|
|
363
|
+
target?: Element | null;
|
|
364
|
+
/** The MouseEvent that triggered the open, when opened via contextmenu or bind. */
|
|
365
|
+
event?: MouseEvent;
|
|
366
|
+
}
|
|
367
|
+
/** Context passed to onClose and onAfterClose when the menu closes. */
|
|
368
|
+
interface CloseContext {
|
|
369
|
+
/** The item that was selected (clicked), if any. */
|
|
370
|
+
selectedItem?: MenuItem;
|
|
371
|
+
/** Anchor coordinates used for the last open, or null. */
|
|
372
|
+
anchor: {
|
|
373
|
+
x: number;
|
|
374
|
+
y: number;
|
|
375
|
+
} | null;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Context menu configuration.
|
|
379
|
+
*/
|
|
380
|
+
interface ContextMenuConfig {
|
|
381
|
+
/** The menu items. */
|
|
382
|
+
menu: MenuItem[] | (() => MenuItem[]);
|
|
383
|
+
/** The configuration for the submenu arrow. */
|
|
384
|
+
submenuArrow?: boolean | SubmenuArrowConfig;
|
|
385
|
+
/** Default configuration for the loading spinner. Overridable per item via loadingIcon, loadingSize, loadingSpeed. */
|
|
386
|
+
spinner?: SpinnerConfig;
|
|
387
|
+
/** Where to open submenus relative to the parent. "auto" uses RTL and viewport space. */
|
|
388
|
+
submenuPlacement?: SubmenuPlacement;
|
|
389
|
+
/** The configuration for the theme. */
|
|
390
|
+
theme?: ThemeConfig;
|
|
391
|
+
/** The configuration for the animations. */
|
|
392
|
+
animation?: AnimationConfig;
|
|
393
|
+
/** The configuration for the position. */
|
|
394
|
+
position?: PositionConfig;
|
|
395
|
+
/** The function to get the anchor. */
|
|
396
|
+
getAnchor?: () => {
|
|
397
|
+
x: number;
|
|
398
|
+
y: number;
|
|
399
|
+
} | DOMRect;
|
|
400
|
+
/** The container to mount the menu. */
|
|
401
|
+
portal?: HTMLElement | (() => HTMLElement);
|
|
402
|
+
/** The function to call when the menu is opened. Receives the MouseEvent when opened via contextmenu or bind; undefined when opened programmatically (e.g. open(x, y) or long-press). */
|
|
403
|
+
onOpen?: (event?: MouseEvent) => void;
|
|
404
|
+
/** The function to call when the menu is closed. Receives close context (selectedItem, anchor). */
|
|
405
|
+
onClose?: (context?: CloseContext) => void;
|
|
406
|
+
/** Called before the menu opens. Return false (or a Promise resolving to false) to cancel. Receives event and open context. */
|
|
407
|
+
onBeforeOpen?: (event?: MouseEvent, context?: OpenContext) => boolean | void | Promise<boolean | void>;
|
|
408
|
+
/** Called after the menu is fully closed (after leave animation). Receives close context. */
|
|
409
|
+
onAfterClose?: (context?: CloseContext) => void;
|
|
410
|
+
/** Called before the menu closes. Return false (or a Promise resolving to false) to cancel. */
|
|
411
|
+
onBeforeClose?: () => boolean | void | Promise<boolean | void>;
|
|
412
|
+
/** Called when the user hovers or focuses an interactive item. */
|
|
413
|
+
onItemHover?: (payload: {
|
|
414
|
+
item: MenuItem;
|
|
415
|
+
nativeEvent: MouseEvent | FocusEvent;
|
|
416
|
+
}) => void;
|
|
417
|
+
/** Element to bind so the menu opens on contextmenu and long-press. Same as calling instance.bind(element, options) after creation. */
|
|
418
|
+
bind?: ContextMenuBindConfig;
|
|
419
|
+
/** When true, close the menu on window resize. */
|
|
420
|
+
closeOnResize?: boolean;
|
|
421
|
+
/** Optional map of shortcut part names to SVG string or HTMLElement. Keys: modifier/key names (e.g. ctrl, shift, enter, tab). When set, shortcuts render these icons instead of Unicode symbols (useful on Windows where symbols may not look good). */
|
|
422
|
+
shortcutIcons?: Record<string, string | HTMLElement>;
|
|
423
|
+
/** Override platform so the menu adapts to that OS (e.g. shortcut display). "auto" (default) = detect. Use "win" on macOS to preview Windows look. */
|
|
424
|
+
platform?: "mac" | "win" | "auto";
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Bind options.
|
|
428
|
+
*/
|
|
429
|
+
interface BindOptions {
|
|
430
|
+
/** The duration of the long press in milliseconds. */
|
|
431
|
+
longPressMs?: number;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Context menu instance.
|
|
435
|
+
*/
|
|
436
|
+
interface ContextMenuInstance {
|
|
437
|
+
/** Opens the menu at coordinates or at the event position. Returns a Promise that resolves with the selected item when the menu closes, or undefined if closed without selection. */
|
|
438
|
+
open(x?: number, y?: number): Promise<MenuItem | undefined>;
|
|
439
|
+
/** Opens the menu at the event position. */
|
|
440
|
+
open(event: MouseEvent): Promise<MenuItem | undefined>;
|
|
441
|
+
/** Closes the menu. Returns a Promise that resolves when the close animation finishes (or immediately if no animation). */
|
|
442
|
+
close(): Promise<void>;
|
|
443
|
+
/** The function to toggle the menu. */
|
|
444
|
+
toggle(x?: number, y?: number): void;
|
|
445
|
+
/** The function to open the menu at an element. */
|
|
446
|
+
openAtElement(element: HTMLElement, options?: OpenAtElementOptions): void;
|
|
447
|
+
/** The function to check if the menu is open. */
|
|
448
|
+
isOpen(): boolean;
|
|
449
|
+
/** Returns the anchor coordinates used for the last open, or null. */
|
|
450
|
+
getAnchor(): {
|
|
451
|
+
x: number;
|
|
452
|
+
y: number;
|
|
453
|
+
} | null;
|
|
454
|
+
/** The function to get the menu. */
|
|
455
|
+
getMenu(): MenuItem[];
|
|
456
|
+
/** The wrapper element (contains root menu and submenus). */
|
|
457
|
+
getRootElement(): HTMLElement;
|
|
458
|
+
/** Update the menu by applying an updater to the current menu. */
|
|
459
|
+
updateMenu(updater: (current: MenuItem[]) => MenuItem[]): void;
|
|
460
|
+
/** The function to bind the menu to an element. */
|
|
461
|
+
bind(element: HTMLElement, options?: BindOptions): void;
|
|
462
|
+
/** Removes bind from the given element, or from the currently bound element if no argument. */
|
|
463
|
+
unbind(element?: HTMLElement): void;
|
|
464
|
+
/** The function to destroy the menu. */
|
|
465
|
+
destroy(): void;
|
|
466
|
+
/** The function to set the menu. */
|
|
467
|
+
setMenu(menu: MenuItem[]): void;
|
|
468
|
+
/** Update theme at runtime; applies to root and open submenus if menu is open. */
|
|
469
|
+
setTheme(theme: ThemeConfig | undefined): void;
|
|
470
|
+
/** Update position config at runtime (used on next open). */
|
|
471
|
+
setPosition(position: PositionConfig | undefined): void;
|
|
472
|
+
/** Update animation config at runtime; applies to root and open submenus if menu is open. */
|
|
473
|
+
setAnimation(animation: AnimationConfig | undefined): void;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Create the context menu.
|
|
478
|
+
* @param config - The configuration.
|
|
479
|
+
* @returns The context menu instance.
|
|
480
|
+
*/
|
|
481
|
+
declare function createContextMenu(config: ContextMenuConfig): ContextMenuInstance;
|
|
482
|
+
|
|
483
|
+
export { type AnimationConfig, type AnimationType, type BadgeConfig, type BindOptions, type CloseContext, type ContextMenuBindConfig, type ContextMenuConfig, type ContextMenuInstance, type EventRegistry, type MenuCheckboxChangeEvent, type MenuClickEvent, type MenuItem, type MenuItemAction, type MenuItemBase, type MenuItemCheckbox, type MenuItemLabel, type MenuItemLink, type MenuItemRadio, type MenuItemSeparator, type MenuItemSubmenu, type MenuItemVariant, type MenuRadioSelectEvent, type OpenAtElementOptions, type OpenAtElementPlacement, type OpenContext, type PositionConfig, type SpinnerConfig, type SubmenuChildren, type SubmenuPlacement, type ThemeConfig, createContextMenu };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e="_cmItem",n="_cmSubmenu",t="cm-menu",o="cm-open",r="cm-leave",i="cm-submenu-open",s="cm-item",l="cm-item-leading",a="--cm-spinner-duration",u="cm-label",c="cm-item-badge",d="cm-submenu",m="[role='menu']",p="data-cm-theme-class",f="--cm-enter-duration",h="--cm-leave-duration",b="--cm-enter-easing",g="--cm-leave-easing",v='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4875 4875" fill="currentColor" aria-hidden="true"><path xmlns="http://www.w3.org/2000/svg" fill="currentColor" d="M0 0h2311v2310H0zm2564 0h2311v2310H2564zM0 2564h2311v2311H0zm2564 0h2311v2311H2564"/></svg>',C='<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6-6-6z"/></svg>',S={cmd:"⌘",alt:"⌥",win:"⌘",windows:"⌘"},y={cmd:"⊞",alt:"⎇",win:{text:"⊞",icon:v},windows:{text:"⊞",icon:v}},w={ctrl:"⌃",shift:"⇧",enter:"↵",return:"↵",tab:"⇥",backspace:"⌫",escape:"⎋",esc:"⎋",delete:"⌦",space:"␣",left:"←",right:"→",up:"↑",down:"↓",arrowleft:"←",arrowright:"→",arrowup:"↑",arrowdown:"↓",home:"⇱",end:"⇲",pageup:"⇞",pagedown:"⇟",insert:"⎀",ins:"⎀"},E=new Set;function T(){const e=document.documentElement;return{vw:e.clientWidth,vh:e.clientHeight}}function H(e,n){for(const[t,o]of Object.entries(n))e.setAttribute(t,o)}function k(e,n){for(const[t,o]of Object.entries(n))t.startsWith("--")?e.style.setProperty(t,o):e.style[t]=o}function x(e,...n){for(const t of n)t&&e.classList.add(...t.trim().split(/\s+/).filter(Boolean))}function A(){if("undefined"==typeof navigator)return!1;const e=navigator.userAgentData?.platform;if("macOS"===e||"iOS"===e)return!0;const n=navigator.userAgent??"";return/Mac|iPhone|iPod|iPad/i.test(n)}function L(e){const n={...e};if("visible"in n&&void 0===n.visible&&(n.visible=!0),"type"in e&&null!=e.type&&""!==e.type||("children"in n?n.type="submenu":"href"in n&&null!=n.href?n.type="link":"label"in n&&!("children"in n)&&(n.type="item")),"children"in n&&"submenu"===n.type){const e=n.children;Array.isArray(e)&&(n.children=e.map(L))}return n}function P(e,n){const t=n.animation;if(!t||t.disabled)return;const o="slide"===t.type?"slide":"fade";e.setAttribute("data-cm-animation",o);const r=t.enter??120,i=t.leave??80,s="number"==typeof r?r:r.duration,l="number"==typeof i?i:i.duration,a="number"==typeof r?"ease-out":r.easing,u="number"==typeof i?"ease-in":i.easing;k(e,{[f]:`${s}ms`,[h]:`${l}ms`,[b]:a,[g]:u})}function I(e,n){const t=document.createElement("span");t.setAttribute("aria-hidden","true"),t.className="cm-icon","string"==typeof n?t.textContent=n:t.appendChild(n),e.appendChild(t)}function O(e,n){if("string"==typeof n||"number"==typeof n){const t=document.createElement("span");return t.setAttribute("aria-hidden","true"),t.className=c,t.textContent=String(n),void e.appendChild(t)}if(n.render){const t=n.render();return t.getAttribute("aria-hidden")||t.setAttribute("aria-hidden","true"),void e.appendChild(t)}const t=document.createElement("span");t.setAttribute("aria-hidden","true"),t.className=c,n.className&&t.classList.add(...n.className.trim().split(/\s+/)),t.textContent=String(n.content??""),e.appendChild(t)}function N(e,n){const t=document.createElement("span");if(t.setAttribute("aria-hidden","true"),t.className="cm-shortcut-icon","string"==typeof n){const e=document.createElement("div");for(e.innerHTML=n;e.firstChild;)t.appendChild(e.firstChild)}else t.appendChild(n.cloneNode(!0));e.appendChild(t)}function z(e,n,t,o){const r=document.createElement("span");r.setAttribute("aria-hidden","true"),r.className="cm-shortcut";const i=function(e,n){if(!e||"string"!=typeof e)return null;const t=e.split("+").map(e=>e.trim());if(0===t.length)return null;const o=t[t.length-1]??"",r=t.slice(0,-1).map(e=>e.toLowerCase()),i=o.toLowerCase(),s=w[i]??(1===o.length?o.toUpperCase():o),l="win"!==n&&("mac"===n||A()),a=l?S:y;return{mods:r.map(e=>{return{name:e,display:l&&"ctrl"===e?S.cmd:w[e]??(n=a[e],t=e,null==n?t:"string"==typeof n?n:n.text)};var n,t}),key:{name:i,display:s},useCmd:l}}(n,o);if(!i)return r.textContent=n,void e.appendChild(r);const{mods:s,key:l,useCmd:a}=i,u=e=>{const n=t?.[e];if(void 0!==n)return n;if("win"===e||"windows"===e){if(a)return;const e=y.win;return"string"==typeof e?void 0:e.icon}};for(const e of s){const n=u(e.name);n?N(r,n):r.appendChild(document.createTextNode(e.display))}const c=u(l.name);c?N(r,c):r.appendChild(document.createTextNode(l.display)),e.appendChild(r)}function R(e,n,t){const o=document.createElement("span");o.className=l,o.setAttribute("aria-hidden","true"),e.appendChild(o);const r=document.createElement("span");return r.className=u,r.textContent=n,e.appendChild(r),t?.icon&&I(e,t.icon),t?.shortcut&&z(e,t.shortcut,t.shortcutIcons,t.platform),void 0!==t?.badge&&O(e,t.badge),o}function M(e,n,t){x(e,"cm-item-loading"),e.setAttribute("aria-busy","true"),function(e,n){const t=e.querySelector(`.${l}`);if(t)K(t,n);else{const t=document.createElement("span");t.className=l,K(t,n),e.insertBefore(t,e.querySelector(`.${u}`)??e.firstChild)}}(e,t?.(n)??{})}function D(e,n,t){n&&(e.id=n),t&&e.setAttribute("aria-disabled","true")}function F(t,o,r){const i=t;i[e]=o,r?._cmCheckbox&&(i._cmCheckbox=r._cmCheckbox),r?._cmRadio&&(i._cmRadio=r._cmRadio),r?._cmSubmenu&&(i[n]=r._cmSubmenu)}function $(e){return e[n]}function _(e,n,t){const o=o=>{var r;t.onHoverFocus?.(e),t.onEnterParentItem?.(e),t.onItemHoverCallback?.(n,o),"disabled"in n&&n.disabled&&(r=e.closest(m))&&(q(r).forEach(e=>e.setAttribute("tabindex","-1")),r.focus()),t.afterFire?.(o)};e.addEventListener("mouseenter",o),e.addEventListener("focus",o),t.onMouseLeave&&e.addEventListener("mouseleave",t.onMouseLeave)}function B(e,n){if(null==n)return;const t="function"==typeof n?n():n;for(const[n,o]of Object.entries(t))null!=o&&("function"==typeof o?e.addEventListener(n,o):e.addEventListener(n,o.listener,o.options))}function W(e){return"number"==typeof e?`${e}px`:e}function K(e,n){const t=document.createElement("span");t.className="cm-spinner",t.setAttribute("aria-hidden","true");const o=n.icon;if(null!=o&&void 0!==o)if(t.classList.add("cm-spinner--custom"),"string"==typeof o){const e=document.createElement("div");for(e.innerHTML=o;e.firstChild;)t.appendChild(e.firstChild)}else t.appendChild(o.cloneNode(!0));const r=n.speed??600,i={[a]:`${r}ms`};if(void 0!==n.size){const e=W(n.size);i.width=e,i.height=e,i.minWidth=e,i.minHeight=e}k(t,i),e.appendChild(t)}function j(e,n,t,o,r,i,l,a,c,d,m,p,f){const h=a??null;if("visible"in e&&!1===e.visible)return null;if("separator"===e.type){const n=document.createElement("div");return n.setAttribute("role","separator"),n.className="cm-separator",x(n,e.className),"events"in e&&e.events&&B(n,e.events),n}if("label"===e.type){const n=e,t=document.createElement("div");t.setAttribute("role","presentation"),t.className=`${s} cm-item-label`,x(t,n.className),D(t,n.id);const o=document.createElement("span");return o.className=u,o.textContent=n.label,t.appendChild(o),"events"in n&&n.events&&B(t,n.events),t}if("checkbox"===e.type||"radio"===e.type)return function(e,n,t){const{close:o,refreshContent:r,getSpinnerOptions:i,onHoverFocus:l,onEnterParentItem:a,onItemHoverCallback:u,shortcutIcons:c,platform:d}=t;let m,p,f,h,b,g,v;if("checkbox"===n?(m="menuitemcheckbox",p="cm-item-checkbox",f="cm-check",h="cm-check--custom",b={_cmCheckbox:e}):(m="menuitemradio",p="cm-item-radio",f="cm-radio",h="cm-radio--custom",b={_cmRadio:e}),e.render?g=e.render(e):(g=document.createElement("div"),g.className=`${s} ${p}`,x(g,e.className,Z(e.variant),e.checked&&"cm-checked"),function(e,n,t,o,r,i,s,l){const a=Boolean(n),u=document.createElement("span");u.setAttribute("aria-hidden","true");const c=null!=t||null!=o;if(u.className=r+(c?` ${i}`:""),a&&s&&u.classList.add(...s.trim().split(/\s+/)),!a&&l&&u.classList.add(...l.trim().split(/\s+/)),c){const e=a?t:o;e&&("string"==typeof e?u.innerHTML=e:u.appendChild(e.cloneNode(!0)))}e.appendChild(u)}(g,e.checked,e.icon,e.uncheckedIcon,f,h,e.checkedClassName,e.uncheckedClassName),R(g,e.label,{icon:e.leadingIcon,shortcut:e.shortcut,shortcutIcons:c,platform:d})),F(g,e,b),H(g,{role:m,"aria-checked":e.checked?"true":"false",tabindex:"-1"}),D(g,e.id,e.disabled),e.loading&&M(g,e,i),_(g,e,{onHoverFocus:l,onEnterParentItem:a,onItemHoverCallback:u}),B(g,e.events),"checkbox"===n){const n=e;v=e=>{n.onChange&&n.onChange({item:n,checked:!n.checked,nativeEvent:e,close:o})}}else{const n=e;v=e=>{n.onSelect&&n.onSelect({item:n,value:n.value,nativeEvent:e,close:o})}}return g.addEventListener("click",n=>{n.preventDefault(),n.stopPropagation(),e.disabled||e.loading||(v(n),r?.(),!1!==e.closeOnAction&&o(e))}),g}(e,e.type,{close:n,refreshContent:c,getSpinnerOptions:m,onHoverFocus:i,onEnterParentItem:l,onItemHoverCallback:d,shortcutIcons:p,platform:f});if("submenu"===e.type){const n=e,a=document.createElement("div");F(a,n,{_cmSubmenu:n}),H(a,{role:"menuitem","aria-haspopup":"menu","aria-expanded":"false",tabindex:"-1"}),a.className=`${s} cm-submenu-trigger`,x(a,n.className,Z(n.variant)),D(a,n.id,n.disabled);const c=document.createElement("span");return c.className=u,c.textContent=n.label,a.appendChild(c),n.icon&&I(a,n.icon),n.shortcut&&z(a,n.shortcut,p,f),void 0!==n.badge&&O(a,n.badge),h&&function(e,n){const t=document.createElement("span");t.setAttribute("aria-hidden","true"),t.className="cm-submenu-arrow",n.className&&t.classList.add(n.className);const o=n.icon??C,r=n.size??14,i={};void 0!==n.opacity&&(i.opacity=String(n.opacity));const s=W(r);if(i.width=s,i.height=s,i.minWidth=s,i.minHeight=s,Object.keys(i).length>0&&k(t,i),t.classList.add("cm-submenu-arrow--icon"),"string"==typeof o){const e=document.createElement("div");for(e.innerHTML=o;e.firstChild;)t.appendChild(e.firstChild)}else t.appendChild(o.cloneNode(!0));e.appendChild(t)}(a,h),_(a,n,{onHoverFocus:i,onEnterParentItem:l,onItemHoverCallback:d,afterFire:()=>{n.disabled||(o?o(n,a):t(n,a))},onMouseLeave:r?()=>r(a):void 0}),a.addEventListener("click",e=>{e.preventDefault(),n.disabled||t(n,a)}),B(a,n.events),a}if("link"===e.type){const t=e,o=document.createElement("a");return o.className=s,x(o,t.className,Z(t.variant)),t.disabled||(o.href=t.href,t.target&&(o.target=t.target),t.rel&&(o.rel=t.rel)),R(o,t.label,{icon:t.icon,shortcut:t.shortcut,badge:t.badge,shortcutIcons:p,platform:f}),F(o,t),H(o,{role:"menuitem",tabindex:"-1"}),D(o,t.id,t.disabled),t.loading&&M(o,t,m),_(o,t,{onHoverFocus:i,onEnterParentItem:l,onItemHoverCallback:d}),o.addEventListener("click",e=>{if(t.disabled||t.loading)return e.preventDefault(),void e.stopPropagation();e.ctrlKey||e.metaKey||(e.preventDefault(),e.stopPropagation(),"_blank"===t.target?window.open(t.href,"_blank",t.rel?`rel=${t.rel}`:void 0):window.location.href=t.href),n(t)}),B(o,t.events),o}const b=e;let g;return b.render?g=b.render(b):(g=document.createElement("div"),g.className=s,x(g,b.className,Z(b.variant)),R(g,b.label,{icon:b.icon,shortcut:b.shortcut,badge:b.badge,shortcutIcons:p,platform:f})),F(g,b),H(g,{role:"menuitem",tabindex:"-1"}),D(g,b.id,b.disabled),b.loading&&M(g,b,m),_(g,b,{onHoverFocus:i,onEnterParentItem:l,onItemHoverCallback:d}),g.addEventListener("click",e=>{if(e.preventDefault(),e.stopPropagation(),b.disabled||b.loading||!b.onClick)return;const t={item:b,nativeEvent:e,close:n};b.onClick(t),!1!==b.closeOnAction&&n(b)}),B(g,b.events),g}function q(e){return Array.from(e.querySelectorAll("[role='menuitem']:not([aria-disabled='true']), [role='menuitemcheckbox']:not([aria-disabled='true']), [role='menuitemradio']:not([aria-disabled='true'])"))}function X(e,n){e.forEach((e,t)=>{e.setAttribute("tabindex",t===n?"0":"-1")}),e[n]&&e[n].focus()}function Y(e){return n=>{const t=q(e),o=t.indexOf(n);o>=0&&X(t,o)}}function U(e){return e.map(e=>{const n={...e};if("children"in n&&"submenu"===n.type){const e=n.children;n.children=Array.isArray(e)?U(e):e}return n})}function Z(e){return e?`cm-item--${e.trim()}`:null}function G(e){return 1===e.length?e.toLowerCase():e}function J(e,n){const t=e.getAttribute(p);if(t&&t.trim().split(/\s+/).forEach(n=>e.classList.remove(n)),e.removeAttribute(p),n?.class&&(x(e,...n.class.trim().split(/\s+/).filter(Boolean)),e.setAttribute(p,n.class)),n?.tokens){const t={};for(const[e,o]of Object.entries(n.tokens))t[e.startsWith("--")?e:"--cm-"+e]=o;k(e,t)}}function Q(e){e.leaveTimeout&&(clearTimeout(e.leaveTimeout),e.leaveTimeout=null),e.leaveTransitionHandler&&(e.root.removeEventListener("transitionend",e.leaveTransitionHandler),e.leaveTransitionHandler=null),e.root.classList.remove(r)}function V(e){E.delete(e.self),e.openPromiseResolve?.(e.lastSelectedItem),e.openPromiseResolve=null,e.closePromiseResolve?.(),e.closePromiseResolve=null}function ee(e){const n=e.currentConfig.animation,t=n?.leave??80,i=n?.disabled?0:"number"==typeof t?t:t.duration,s={selectedItem:e.lastSelectedItem,anchor:e.lastAnchor};if(i>0&&!n?.disabled){e.root.classList.remove(o),e.root.classList.add(r);const n=()=>{e.leaveTimeout&&clearTimeout(e.leaveTimeout),e.leaveTimeout=null,e.leaveTransitionHandler&&(e.root.removeEventListener("transitionend",e.leaveTransitionHandler),e.leaveTransitionHandler=null),e.root.classList.remove(r),k(e.root,{display:"none"}),e.wrapper.remove(),V(e),e.currentConfig.onClose?.(s),e.currentConfig.onAfterClose?.(s),e.lastFocusTarget&&"function"==typeof e.lastFocusTarget.focus&&e.lastFocusTarget.focus()};e.leaveTransitionHandler=n,e.root.addEventListener("transitionend",n,{once:!0}),e.leaveTimeout=setTimeout(n,i+50)}else k(e.root,{display:"none"}),e.wrapper.remove(),V(e),e.currentConfig.onClose?.(s),e.currentConfig.onAfterClose?.(s),e.lastFocusTarget&&"function"==typeof e.lastFocusTarget.focus&&e.lastFocusTarget.focus()}function ne(e,n,t,s={}){const{clearOpenSubmenu:l=!0,onDone:a}=s,u=t.currentConfig.animation,c=u?.leave??80,d=u?.disabled?0:"number"==typeof c?c:c.duration,m=()=>{if(e.remove(),n.setAttribute("aria-expanded","false"),n.classList.remove(i),l){const n=t.openSubmenus.findIndex(n=>n.panel===e);n>=0&&t.openSubmenus.splice(n,1)}a?.()};if(d<=0||u?.disabled)return void m();e.classList.remove(o),e.classList.add(r);let p=!1;const f=()=>{p||(p=!0,e.removeEventListener("transitionend",f),h&&clearTimeout(h),m())};e.addEventListener("transitionend",f,{once:!0});const h=setTimeout(f,d+50)}function te(e){return new Promise(n=>{(async()=>{if(!1!==await Promise.resolve(e.currentConfig.onBeforeClose?.()))if(e.isOpen){if(e.closePromiseResolve=n,e.isOpen=!1,e.outsideClickHandler&&(document.removeEventListener("mousedown",e.outsideClickHandler,!0),e.outsideClickHandler=null),e.resizeHandler&&(window.removeEventListener("resize",e.resizeHandler),e.resizeHandler=null),Q(e),e.submenuHoverTimer&&clearTimeout(e.submenuHoverTimer),e.submenuHoverTimer=null,e.openSubmenus.length>0){const n=e.openSubmenus.slice();e.openSubmenus.length=0;let t=n.length-1;const o=()=>{if(t<0)return void ee(e);const{panel:r,trigger:i}=n[t];t--,ne(r,i,e,{clearOpenSubmenu:!1,onDone:o})};return void o()}ee(e)}else n();else n()})()})}function oe(e,n,t){return new Promise(r=>{e.openPromiseResolve=r,(async()=>{"function"==typeof e.currentConfig.menu&&(e.menu=e.currentConfig.menu().map(L));const r="object"==typeof n&&null!==n?n:void 0;let i,s;if(r)i=r.clientX,s=r.clientY;else if(void 0===n&&void 0===t&&e.currentConfig.getAnchor){const n=function(e){if("width"in e&&"height"in e){const n=e;return{x:n.left+n.width/2,y:n.top}}return e}(e.currentConfig.getAnchor());i=n.x,s=n.y}else i=n??0,s=t??0;const l={x:i,y:s,target:r?.target instanceof Element?r.target:null,event:r};if(!1===await Promise.resolve(e.currentConfig.onBeforeOpen?.(r,l)))return e.openPromiseResolve?.(void 0),void(e.openPromiseResolve=null);E.add(e.self);const a=[...E].filter(n=>n!==e.self);await Promise.all(a.map(e=>e.close())),Q(e),e.isOpen&&await e.realClose(),e.lastAnchor={x:i,y:s},e.lastSelectedItem=void 0,e.lastFocusTarget=document.activeElement,e.isOpen=!0,e.buildRootContent(),e.wrapper.parentElement||e.portal.appendChild(e.wrapper),e.outsideClickHandler=n=>{e.wrapper.contains(n.target)||e.realClose()},document.addEventListener("mousedown",e.outsideClickHandler,!0),e.currentConfig.closeOnResize&&(e.resizeHandler=()=>{e.realClose()},window.addEventListener("resize",e.resizeHandler)),function(e,n,t,o){const r=o.position??{},i=r.offset?.x??0,s=r.offset?.y??0,l=r.padding??8,a=!1!==r.flip,u=!1!==r.shift;k(e,{display:""}),e.getClientRects();const c=e.getBoundingClientRect(),{vw:d,vh:m}=T();let p=n+i,f=t+s;a&&(f+c.height>m-l&&(f=t-c.height-s),p+c.width>d-l&&(p=n-c.width-i),p<l&&(p=l),f<l&&(f=l)),u&&(p=Math.max(l,Math.min(d-c.width-l,p)),f=Math.max(l,Math.min(m-c.height-l,f))),k(e,{left:`${p}px`,top:`${f}px`})}(e.root,i,s,e.currentConfig),k(e.root,{display:""});const u=()=>{e.root.classList.add(o),e.currentConfig.onOpen?.(r);const n=q(e.root);n.length&&X(n,0)},c=e.currentConfig.animation;c?.disabled?u():(e.root.getClientRects(),requestAnimationFrame(u))})()})}function re(e){e.longPressTimer&&(clearTimeout(e.longPressTimer),e.longPressTimer=null)}function ie(e,n){null!=n&&e.boundElement!==n||e.boundElement&&(re(e),e.boundContextmenu&&e.boundElement.removeEventListener("contextmenu",e.boundContextmenu),e.boundTouchstart&&e.boundElement.removeEventListener("touchstart",e.boundTouchstart),e.boundElement.removeEventListener("touchend",e.boundTouchEndOrCancel),e.boundElement.removeEventListener("touchcancel",e.boundTouchEndOrCancel),e.boundElement=null,e.boundContextmenu=null,e.boundTouchstart=null)}function se(e,n,t){ie(e);const o=t?.longPressMs??500;e.boundContextmenu=n=>{n.preventDefault(),"pointerType"in n&&"touch"===n.pointerType||oe(e,n)},e.boundTouchstart=n=>{1===n.touches.length&&(re(e),e.longPressX=n.touches[0].clientX,e.longPressY=n.touches[0].clientY,e.longPressTimer=setTimeout(()=>{e.longPressTimer=null,oe(e,e.longPressX,e.longPressY)},o))},e.boundTouchEndOrCancel=()=>re(e),n.addEventListener("contextmenu",e.boundContextmenu),n.addEventListener("touchstart",e.boundTouchstart,{passive:!0}),n.addEventListener("touchend",e.boundTouchEndOrCancel,{passive:!0}),n.addEventListener("touchcancel",e.boundTouchEndOrCancel,{passive:!0}),e.boundElement=n}function le(e,n){e.menu=n.map(L),e.isOpen&&e.buildRootContent()}function ae(r){const s={...r},l=("function"==typeof s.menu?s.menu():s.menu??[]).map(L),a=function(e){return null==e?document.body:"function"==typeof e?e():e}(s.portal),u=document.createElement("div");u.className="cm-wrapper";const c=document.createElement("div");H(c,{role:"menu","aria-orientation":"vertical",tabindex:"-1"}),c.className=t,k(c,{display:"none",...null!=s.position?.zIndexBase?{zIndex:String(s.position.zIndexBase)}:{}}),J(c,s.theme),P(c,s),u.appendChild(c);const p={currentConfig:s,menu:l,portal:a,wrapper:u,root:c,submenuArrowConfig:(f=s.submenuArrow,!1===f||void 0===f?null:!0===f?{icon:C,size:14}:f),isOpen:!1,lastFocusTarget:null,leaveTimeout:null,leaveTransitionHandler:null,openSubmenus:[],submenuHoverTimer:null,outsideClickHandler:null,resizeHandler:null,boundElement:null,boundContextmenu:null,boundTouchstart:null,boundTouchEndOrCancel:()=>{},longPressTimer:null,longPressX:0,longPressY:0,lastAnchor:null,lastSelectedItem:void 0,openPromiseResolve:null,closePromiseResolve:null,self:null,closeWithSelection:null,realClose:null,openSubmenuPanel:null,scheduleSubmenuOpen:null,scheduleSubmenuClose:null,cancelSubmenuClose:null,closeSubmenuWithAnimation:null,buildRootContent:null,refreshContent:null,getSpinnerOptions:null,makeHoverFocusHandler:null,onEnterMenuItem:null,triggerSubmenu:null,_keydownHandler:null};var f;p.self={close:()=>te(p)},p.closeWithSelection=e=>{void 0!==e&&(p.lastSelectedItem=e),te(p)},p.realClose=()=>te(p),p.openSubmenuPanel=(e,n)=>async function(e,n,r){let s=-1;if(e.root.contains(r))s=-1;else for(let n=0;n<e.openSubmenus.length;n++)if(e.openSubmenus[n].panel.contains(r)){s=n;break}for(let n=e.openSubmenus.length-1;n>s;n--){const{panel:t,trigger:o}=e.openSubmenus[n];e.closeSubmenuWithAnimation(t,o,{clearOpenSubmenu:!0})}const l=await async function(e){return(Array.isArray(e)?e:await e()).map(L)}(n.children),a=document.createElement("div");H(a,{role:"menu","aria-label":n.label,"aria-orientation":"vertical",tabindex:"-1"}),a.className=`${t} ${d}`,a.addEventListener("mouseenter",e.cancelSubmenuClose),J(a,e.currentConfig.theme),P(a,e.currentConfig);const u=e.currentConfig.position?.submenuZIndexStep??0,c=e.currentConfig.position?.zIndexBase??9999;u>0&&k(a,{zIndex:String(c+(e.openSubmenus.length+1)*u)}),l.forEach(n=>{const t=j(n,e.closeWithSelection,(n,t)=>{e.openSubmenuPanel(n,t)},e.scheduleSubmenuOpen,e.scheduleSubmenuClose,e.makeHoverFocusHandler(a),e.onEnterMenuItem,e.submenuArrowConfig,e.refreshContent,(n,t)=>e.currentConfig.onItemHover?.({item:n,nativeEvent:t}),e.getSpinnerOptions,e.currentConfig.shortcutIcons,e.currentConfig.platform);t&&a.appendChild(t)}),e.wrapper.appendChild(a);const m=r.getBoundingClientRect(),p=e.currentConfig.position?.padding??8,{vw:f,vh:h}=T(),b="rtl"===getComputedStyle(r).direction,g=n.submenuPlacement??e.currentConfig.submenuPlacement??"auto";k(a,{display:""}),a.getClientRects();const v=a.getBoundingClientRect();let C;"left"===g?(C=m.left-v.width-2,C<p&&(C=m.right+2)):"right"===g?(C=m.right+2,C+v.width>f-p&&(C=m.left-v.width-2)):b?(C=m.left-v.width-2,C<p&&(C=m.right+2)):(C=m.right+2,C+v.width>f-p&&(C=m.left-v.width-2));let S=m.top;S+v.height>h-p&&(S=h-v.height-p),S<p&&(S=p),k(a,{left:`${C}px`,top:`${S}px`}),r.setAttribute("aria-expanded","true"),r.classList.add(i),e.openSubmenus.push({panel:a,trigger:r}),requestAnimationFrame(()=>a.classList.add(o))}(p,e,n),p.scheduleSubmenuOpen=(e,n)=>function(e,n,t){const o=e.openSubmenus[e.openSubmenus.length-1];o&&o.trigger===t?t.focus():(e.submenuHoverTimer&&clearTimeout(e.submenuHoverTimer),e.submenuHoverTimer=setTimeout(()=>{e.submenuHoverTimer=null;const o=e.openSubmenus[e.openSubmenus.length-1];o&&o.trigger===t||e.openSubmenuPanel(n,t)},200))}(p,e,n),p.scheduleSubmenuClose=e=>function(e,n){e.submenuHoverTimer&&clearTimeout(e.submenuHoverTimer),e.submenuHoverTimer=setTimeout(()=>{e.submenuHoverTimer=null;const t=e.openSubmenus.findIndex(e=>e.trigger===n);if(!(t<0))for(let n=e.openSubmenus.length-1;n>=t;n--){const{panel:t,trigger:o}=e.openSubmenus[n];e.closeSubmenuWithAnimation(t,o,{clearOpenSubmenu:!0})}},150)}(p,e),p.cancelSubmenuClose=()=>function(e){e.submenuHoverTimer&&clearTimeout(e.submenuHoverTimer),e.submenuHoverTimer=null}(p),p.closeSubmenuWithAnimation=(e,n,t)=>ne(e,n,p,t),p.buildRootContent=()=>function(e){e.root.innerHTML="",e.menu.forEach(n=>{const t=j(n,e.closeWithSelection,e.triggerSubmenu,e.scheduleSubmenuOpen,e.scheduleSubmenuClose,e.makeHoverFocusHandler(e.root),n=>e.onEnterMenuItem(n),e.submenuArrowConfig,e.refreshContent,(n,t)=>e.currentConfig.onItemHover?.({item:n,nativeEvent:t}),e.getSpinnerOptions,e.currentConfig.shortcutIcons,e.currentConfig.platform);t&&e.root.appendChild(t)})}(p),p.refreshContent=()=>function(e){e.isOpen&&"function"==typeof e.currentConfig.menu&&(e.menu=e.currentConfig.menu().map(L),e.buildRootContent())}(p),p.getSpinnerOptions=e=>function(e,n){const t=e.currentConfig.spinner??{};return n&&"object"==typeof n&&("loadingIcon"in n||"loadingSize"in n||"loadingSpeed"in n)?{...t,..."loadingIcon"in n&&void 0!==n.loadingIcon&&{icon:n.loadingIcon},..."loadingSize"in n&&void 0!==n.loadingSize&&{size:n.loadingSize},..."loadingSpeed"in n&&void 0!==n.loadingSpeed&&{speed:n.loadingSpeed}}:t}(p,e),p.makeHoverFocusHandler=Y,p.onEnterMenuItem=e=>function(e,n){if(0===e.openSubmenus.length)return;e.cancelSubmenuClose();const t=n.closest(m);if(!t)return;let o=-1;if(t!==e.root)for(let n=0;n<e.openSubmenus.length;n++)if(e.openSubmenus[n].panel===t){o=n;break}for(let n=e.openSubmenus.length-1;n>o;n--){const{panel:t,trigger:o}=e.openSubmenus[n];e.closeSubmenuWithAnimation(t,o,{clearOpenSubmenu:!0})}}(p,e),p.triggerSubmenu=(e,n)=>{p.openSubmenuPanel(e,n)},p._keydownHandler=t=>function(t,o){const r=o.target,i=r.closest(m);if(!i)return;const s=i.classList.contains(d),l=q(i);let a=l.indexOf(r);if(-1!==a)switch(o.key){case"ArrowDown":o.preventDefault(),X(l,(a+1)%l.length);break;case"ArrowUp":o.preventDefault(),X(l,0===a?l.length-1:a-1);break;case"ArrowRight":{o.preventDefault();const e=$(r);e&&t.openSubmenuPanel(e,r);break}case"ArrowLeft":if(o.preventDefault(),s&&t.openSubmenus.length>0){const{panel:e,trigger:n}=t.openSubmenus[t.openSubmenus.length-1];t.closeSubmenuWithAnimation(e,n,{clearOpenSubmenu:!0,onDone:()=>n.focus()})}else t.realClose();break;case"Enter":case" ":{o.preventDefault();const e=$(r);e?t.openSubmenuPanel(e,r):r.click();break}case"Escape":if(o.preventDefault(),s&&t.openSubmenus.length>0){const{panel:e,trigger:n}=t.openSubmenus[t.openSubmenus.length-1];t.closeSubmenuWithAnimation(e,n,{clearOpenSubmenu:!0,onDone:()=>n.focus()})}else t.realClose();break;case"Home":o.preventDefault(),X(l,0);break;case"End":o.preventDefault(),X(l,l.length-1);break;default:{const r=l.find(t=>{const r=function(t){const o=t;return o[n]??o[e]}(t);return!!(r&&"shortcut"in r&&r.shortcut)&&function(e,n){const t=e.split("+").map(e=>e.trim()).filter(Boolean);if(0===t.length)return!1;if(G(t[t.length-1]??"")!==G(n.key))return!1;const o=t.slice(0,-1).map(e=>e.toLowerCase());if(o.includes("ctrl")||o.includes("cmd")){if(!(A()?n.metaKey&&!n.ctrlKey:n.ctrlKey&&!n.metaKey))return!1}else if(n.ctrlKey||n.metaKey)return!1;const r=(e,n)=>o.includes(e)?n:!n;return!!r("alt",n.altKey)&&!!r("shift",n.shiftKey)}(r.shortcut,o)});if(r){o.preventDefault();const e=$(r);e?(t.openSubmenuPanel(e,r),requestAnimationFrame(()=>{const e=t.openSubmenus[t.openSubmenus.length-1];if(!e)return;const n=q(e.panel);n.length&&X(n,0)})):r.click()}break}}else"ArrowDown"===o.key&&l.length?(o.preventDefault(),X(l,0)):"ArrowUp"===o.key&&l.length&&(o.preventDefault(),X(l,l.length-1))}(p,t),u.addEventListener("keydown",p._keydownHandler);const h=s.bind;if(null!=h){const e=h instanceof HTMLElement?h:h.element;se(p,e,h instanceof HTMLElement?void 0:h.options)}return function(e){return{open:(n,t)=>oe(e,n,t),close:()=>e.realClose(),toggle(n,t){e.isOpen?e.realClose():oe(e,n??0,t??0)},openAtElement:(n,t)=>function(e,n,t){const o=t?.offset??{x:0,y:0},r=n.getBoundingClientRect();let i,s,l=t?.placement??"bottom-start";if("auto"===l){const t=e.currentConfig.position?.padding??8,{vw:o,vh:i}=T(),s=r.top-t,a=i-r.bottom-t,u=r.left-t,c=o-r.right-t,d=a>=s?"bottom":"top",m="rtl"===getComputedStyle(n).direction;l=`${d}-${(m?u:c)>=(m?c:u)?m?"end":"start":m?"start":"end"}`}switch(l){case"bottom-start":case"left-end":default:i=r.left,s=r.bottom;break;case"bottom-end":case"right-end":i=r.right,s=r.bottom;break;case"top-start":case"left-start":i=r.left,s=r.top;break;case"top-end":case"right-start":i=r.right,s=r.top}oe(e,i+o.x,s+o.y)}(e,n,t),isOpen:()=>e.isOpen,getAnchor:()=>e.lastAnchor,getMenu:()=>U(e.menu),getRootElement:()=>e.wrapper,updateMenu:n=>le(e,n(U(e.menu))),bind:(n,t)=>se(e,n,t),unbind:n=>ie(e,n),destroy:()=>function(e){ie(e),e.outsideClickHandler&&(document.removeEventListener("mousedown",e.outsideClickHandler,!0),e.outsideClickHandler=null),e.resizeHandler&&(window.removeEventListener("resize",e.resizeHandler),e.resizeHandler=null),e.leaveTimeout&&clearTimeout(e.leaveTimeout),e.submenuHoverTimer&&clearTimeout(e.submenuHoverTimer),e.wrapper.remove(),e.wrapper.removeEventListener("keydown",e._keydownHandler)}(e),setMenu:n=>le(e,n),setTheme:n=>function(e,n){e.currentConfig.theme=n,J(e.root,n);for(const{panel:t}of e.openSubmenus)J(t,n)}(e,n),setPosition:n=>function(e,n){e.currentConfig.position=n}(e,n),setAnimation:n=>function(e,n){e.currentConfig.animation=n,P(e.root,e.currentConfig);for(const{panel:n}of e.openSubmenus)P(n,e.currentConfig)}(e,n)}}(p)}export{ae as createContextMenu};//# sourceMappingURL=index.js.map
|