tycho-components 0.8.24 → 0.9.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/dist/CytoscapeMenu/CytoscapeMenuCanvas.d.ts +18 -0
- package/dist/CytoscapeMenu/CytoscapeMenuCanvas.js +95 -0
- package/dist/CytoscapeMenu/constants.d.ts +38 -0
- package/dist/CytoscapeMenu/constants.js +15 -0
- package/dist/CytoscapeMenu/context-menu.d.ts +78 -0
- package/dist/CytoscapeMenu/context-menu.js +408 -0
- package/dist/CytoscapeMenu/cytoscape-context-menus.d.ts +22 -0
- package/dist/CytoscapeMenu/cytoscape-context-menus.js +328 -0
- package/dist/CytoscapeMenu/index.d.ts +6 -0
- package/dist/CytoscapeMenu/index.js +13 -0
- package/dist/CytoscapeMenu/style.scss +45 -0
- package/dist/CytoscapeMenu/utils.d.ts +17 -0
- package/dist/CytoscapeMenu/utils.js +72 -0
- package/dist/cytoscape-context-menus.d.ts +5 -0
- package/dist/cytoscape-context-menus.js +5 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +6 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import cytoscape, { Core } from 'cytoscape';
|
|
2
|
+
import type { CytoscapeContextMenuOptions, MenuItemOption } from './constants';
|
|
3
|
+
import './style.scss';
|
|
4
|
+
export interface CytoscapeMenuCanvasProps {
|
|
5
|
+
/** Unique id for the container element */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Cytoscape elements (nodes and edges) */
|
|
8
|
+
elements?: cytoscape.ElementDefinition[];
|
|
9
|
+
/** Context menu options */
|
|
10
|
+
menuItems?: MenuItemOption[];
|
|
11
|
+
/** Called when cytoscape instance is ready */
|
|
12
|
+
onReady?: (cy: Core) => void;
|
|
13
|
+
/** Additional context menu options */
|
|
14
|
+
contextMenuOptions?: Omit<CytoscapeContextMenuOptions, 'menuItems'>;
|
|
15
|
+
/** Container height */
|
|
16
|
+
height?: string | number;
|
|
17
|
+
}
|
|
18
|
+
export default function CytoscapeMenuCanvas({ id, elements, menuItems, onReady, contextMenuOptions, height, }: CytoscapeMenuCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import cytoscape from 'cytoscape';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { registerCytoscapeContextMenus } from './index';
|
|
5
|
+
import './style.scss';
|
|
6
|
+
registerCytoscapeContextMenus(cytoscape);
|
|
7
|
+
const DEFAULT_ELEMENTS = [
|
|
8
|
+
{ data: { id: 'a', label: 'Node A' } },
|
|
9
|
+
{ data: { id: 'b', label: 'Node B' } },
|
|
10
|
+
{ data: { id: 'c', label: 'Node C' } },
|
|
11
|
+
{ data: { id: 'ab', source: 'a', target: 'b' } },
|
|
12
|
+
{ data: { id: 'bc', source: 'b', target: 'c' } },
|
|
13
|
+
];
|
|
14
|
+
export default function CytoscapeMenuCanvas({ id = 'cytoscape-menu-canvas', elements = DEFAULT_ELEMENTS, menuItems = [], onReady, contextMenuOptions = {}, height = '400px', }) {
|
|
15
|
+
const cyRef = useRef(null);
|
|
16
|
+
const containerRef = useRef(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const container = document.getElementById(id);
|
|
19
|
+
if (!container)
|
|
20
|
+
return;
|
|
21
|
+
container.innerHTML = '';
|
|
22
|
+
const cy = cytoscape({
|
|
23
|
+
container,
|
|
24
|
+
elements,
|
|
25
|
+
style: [
|
|
26
|
+
{
|
|
27
|
+
selector: 'node',
|
|
28
|
+
style: {
|
|
29
|
+
label: 'data(label)',
|
|
30
|
+
'background-color': '#4a90d9',
|
|
31
|
+
color: '#fff',
|
|
32
|
+
'text-valign': 'center',
|
|
33
|
+
'text-halign': 'center',
|
|
34
|
+
padding: '8px',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
selector: 'edge',
|
|
39
|
+
style: {
|
|
40
|
+
width: 2,
|
|
41
|
+
'line-color': '#ccc',
|
|
42
|
+
'target-arrow-color': '#ccc',
|
|
43
|
+
'target-arrow-shape': 'triangle',
|
|
44
|
+
'curve-style': 'bezier',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
layout: { name: 'grid' },
|
|
49
|
+
});
|
|
50
|
+
const menuOptions = {
|
|
51
|
+
menuItems: menuItems.length > 0 ? menuItems : [
|
|
52
|
+
{
|
|
53
|
+
id: 'remove',
|
|
54
|
+
content: 'Remove',
|
|
55
|
+
tooltipText: 'Remove element',
|
|
56
|
+
selector: 'node, edge',
|
|
57
|
+
onClickFunction: (event) => {
|
|
58
|
+
event.target.remove();
|
|
59
|
+
},
|
|
60
|
+
hasTrailingDivider: true,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'select',
|
|
64
|
+
content: 'Select',
|
|
65
|
+
tooltipText: 'Select element',
|
|
66
|
+
selector: 'node, edge',
|
|
67
|
+
onClickFunction: (event) => {
|
|
68
|
+
cy.elements().unselect();
|
|
69
|
+
event.target.select();
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'fit',
|
|
74
|
+
content: 'Fit to view',
|
|
75
|
+
tooltipText: 'Fit graph to view',
|
|
76
|
+
selector: 'core',
|
|
77
|
+
coreAsWell: true,
|
|
78
|
+
onClickFunction: () => {
|
|
79
|
+
cy.fit();
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
...contextMenuOptions,
|
|
84
|
+
};
|
|
85
|
+
const contextMenusInstance = cy.contextMenus(menuOptions);
|
|
86
|
+
cyRef.current = cy;
|
|
87
|
+
onReady?.(cy);
|
|
88
|
+
return () => {
|
|
89
|
+
contextMenusInstance.destroy();
|
|
90
|
+
cy.destroy();
|
|
91
|
+
cyRef.current = null;
|
|
92
|
+
};
|
|
93
|
+
}, [id, elements, menuItems, contextMenuOptions, onReady]);
|
|
94
|
+
return (_jsx("div", { ref: containerRef, id: id, style: { height: typeof height === 'number' ? `${height}px` : height, width: '100%' }, className: "cytoscape-menu-canvas" }));
|
|
95
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const CXT_MENU_CSS_CLASS = "cy-context-menus-cxt-menu";
|
|
2
|
+
export declare const MENUITEM_CSS_CLASS = "cy-context-menus-cxt-menuitem";
|
|
3
|
+
export declare const DIVIDER_CSS_CLASS = "cy-context-menus-divider";
|
|
4
|
+
export declare const INDICATOR_CSS_CLASS = "cy-context-menus-submenu-indicator";
|
|
5
|
+
export interface MenuItemImage {
|
|
6
|
+
src: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
y: string;
|
|
10
|
+
x: string;
|
|
11
|
+
}
|
|
12
|
+
import type { EventObject } from 'cytoscape';
|
|
13
|
+
export interface MenuItemOption {
|
|
14
|
+
id: string;
|
|
15
|
+
content: string;
|
|
16
|
+
tooltipText?: string;
|
|
17
|
+
selector: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
image?: MenuItemImage;
|
|
20
|
+
show?: boolean;
|
|
21
|
+
submenu?: MenuItemOption[];
|
|
22
|
+
coreAsWell?: boolean;
|
|
23
|
+
onClickFunction?: (event: EventObject) => void;
|
|
24
|
+
hasTrailingDivider?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface SubmenuIndicatorOption {
|
|
27
|
+
src: string;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
}
|
|
31
|
+
export interface CytoscapeContextMenuOptions {
|
|
32
|
+
evtType?: string;
|
|
33
|
+
menuItems?: MenuItemOption[];
|
|
34
|
+
menuItemClasses?: string[];
|
|
35
|
+
contextMenuClasses?: string[];
|
|
36
|
+
submenuIndicator?: SubmenuIndicatorOption;
|
|
37
|
+
}
|
|
38
|
+
export declare const DEFAULT_OPTS: CytoscapeContextMenuOptions;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const CXT_MENU_CSS_CLASS = 'cy-context-menus-cxt-menu';
|
|
2
|
+
export const MENUITEM_CSS_CLASS = 'cy-context-menus-cxt-menuitem';
|
|
3
|
+
export const DIVIDER_CSS_CLASS = 'cy-context-menus-divider';
|
|
4
|
+
export const INDICATOR_CSS_CLASS = 'cy-context-menus-submenu-indicator';
|
|
5
|
+
export const DEFAULT_OPTS = {
|
|
6
|
+
evtType: 'cxttap',
|
|
7
|
+
menuItems: [],
|
|
8
|
+
menuItemClasses: [MENUITEM_CSS_CLASS],
|
|
9
|
+
contextMenuClasses: [CXT_MENU_CSS_CLASS],
|
|
10
|
+
submenuIndicator: {
|
|
11
|
+
src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDEyIDEyIj48cGF0aCBmaWxsPSIjNjY2IiBkPSJNNCA0IEw4IDggTDQgMTIiLz48L3N2Zz4=',
|
|
12
|
+
width: 12,
|
|
13
|
+
height: 12,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { EventObject } from 'cytoscape';
|
|
2
|
+
import type { MenuItemOption } from './constants';
|
|
3
|
+
export interface Scratchpad {
|
|
4
|
+
cxtMenuItemClasses: string;
|
|
5
|
+
cxtMenuClasses: string;
|
|
6
|
+
submenuIndicatorGen: () => HTMLImageElement;
|
|
7
|
+
currentCyEvent?: EventObject;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export declare class MenuItem extends HTMLButtonElement {
|
|
11
|
+
onMenuItemClick: (event: Event) => void;
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
clickFns: (() => void)[];
|
|
14
|
+
selector: string;
|
|
15
|
+
hasTrailingDivider?: boolean;
|
|
16
|
+
show: boolean;
|
|
17
|
+
coreAsWell: boolean;
|
|
18
|
+
scratchpad: Scratchpad;
|
|
19
|
+
onClickFunction?: (event: EventObject) => void;
|
|
20
|
+
submenu?: MenuItemList;
|
|
21
|
+
indicator?: HTMLImageElement;
|
|
22
|
+
mouseEnterHandler?: (event: MouseEvent) => void;
|
|
23
|
+
mouseLeaveHandler?: (event: MouseEvent) => void;
|
|
24
|
+
constructor(params: MenuItemOption, onMenuItemClick: (event: Event) => void, scratchpad: Scratchpad);
|
|
25
|
+
bindOnClickFunction(onClickFn: () => void): void;
|
|
26
|
+
unbindOnClickFunctions(): void;
|
|
27
|
+
enable(): void;
|
|
28
|
+
disable(): void;
|
|
29
|
+
hide(): void;
|
|
30
|
+
getHasTrailingDivider(): boolean;
|
|
31
|
+
setHasTrailingDivider(status: boolean): void;
|
|
32
|
+
hasSubmenu(): boolean;
|
|
33
|
+
appendSubmenuItem(menuItem: MenuItem, before?: Element): void;
|
|
34
|
+
isClickable(): boolean;
|
|
35
|
+
display(): void;
|
|
36
|
+
isVisible(): boolean;
|
|
37
|
+
removeSubmenu(): void;
|
|
38
|
+
detachSubmenu(): void;
|
|
39
|
+
_onMouseEnter(_event: MouseEvent): void;
|
|
40
|
+
_onMouseLeave(event: MouseEvent): void;
|
|
41
|
+
_createSubmenu(items?: MenuItemOption[]): void;
|
|
42
|
+
_getMenuItemClassStr(classStr: string, hasTrailingDivider?: boolean): string;
|
|
43
|
+
static define(): void;
|
|
44
|
+
}
|
|
45
|
+
export declare class MenuItemList extends HTMLDivElement {
|
|
46
|
+
onMenuItemClick: (event: Event) => void;
|
|
47
|
+
scratchpad: Scratchpad;
|
|
48
|
+
constructor(onMenuItemClick: (event: Event) => void, scratchpad: Scratchpad);
|
|
49
|
+
hide(): void;
|
|
50
|
+
display(): void;
|
|
51
|
+
isVisible(): boolean;
|
|
52
|
+
hideMenuItems(): void;
|
|
53
|
+
hideSubmenus(): void;
|
|
54
|
+
appendMenuItem(menuItem: MenuItem, before?: Element): void;
|
|
55
|
+
moveBefore(menuItem: MenuItem, before: MenuItem): void;
|
|
56
|
+
removeAllMenuItems(): void;
|
|
57
|
+
_removeImmediateMenuItem(menuItem: MenuItem): void;
|
|
58
|
+
_detachImmediateMenuItem(menuItem: MenuItem): boolean;
|
|
59
|
+
_performBindings(menuItem: MenuItem): void;
|
|
60
|
+
_bindOnClick(onClickFn: (event: EventObject) => void): () => void;
|
|
61
|
+
static define(): void;
|
|
62
|
+
}
|
|
63
|
+
export declare class ContextMenu extends MenuItemList {
|
|
64
|
+
constructor(onMenuItemClick: () => void, scratchpad: Scratchpad);
|
|
65
|
+
removeMenuItem(menuItem: MenuItem): void;
|
|
66
|
+
appendMenuItem(menuItem: MenuItem, before?: Element): void;
|
|
67
|
+
insertMenuItem(menuItem: MenuItem, { before, parent }?: {
|
|
68
|
+
before?: MenuItem;
|
|
69
|
+
parent?: MenuItem;
|
|
70
|
+
}): void;
|
|
71
|
+
moveBefore(menuItem: MenuItem, before: MenuItem): void;
|
|
72
|
+
moveToSubmenu(menuItem: MenuItem, parent?: MenuItem | null, options?: {
|
|
73
|
+
selector?: string;
|
|
74
|
+
coreAsWell?: boolean;
|
|
75
|
+
} | null): void;
|
|
76
|
+
ensureDoesntContain(id: string): void;
|
|
77
|
+
static define(): void;
|
|
78
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { setBooleanAttribute, isIn, getDimensionsHidden, defineCustomElement, } from './utils';
|
|
2
|
+
import { DIVIDER_CSS_CLASS } from './constants';
|
|
3
|
+
function stopEvent(event) {
|
|
4
|
+
event.preventDefault();
|
|
5
|
+
event.stopPropagation();
|
|
6
|
+
}
|
|
7
|
+
export class MenuItem extends HTMLButtonElement {
|
|
8
|
+
constructor(params, onMenuItemClick, scratchpad) {
|
|
9
|
+
super();
|
|
10
|
+
this.clickFns = [];
|
|
11
|
+
super.setAttribute('id', params.id);
|
|
12
|
+
const className = this._getMenuItemClassStr(scratchpad['cxtMenuItemClasses'], params.hasTrailingDivider);
|
|
13
|
+
super.setAttribute('class', className);
|
|
14
|
+
super.setAttribute('title', params.tooltipText ?? '');
|
|
15
|
+
if (params.disabled) {
|
|
16
|
+
setBooleanAttribute(this, 'disabled', true);
|
|
17
|
+
}
|
|
18
|
+
if (params.image) {
|
|
19
|
+
const img = document.createElement('img');
|
|
20
|
+
img.src = params.image.src;
|
|
21
|
+
img.width = params.image.width;
|
|
22
|
+
img.height = params.image.height;
|
|
23
|
+
img.style.position = 'absolute';
|
|
24
|
+
img.style.top = params.image.y + 'px';
|
|
25
|
+
img.style.left = params.image.x + 'px';
|
|
26
|
+
super.appendChild(img);
|
|
27
|
+
}
|
|
28
|
+
this.innerHTML += params.content;
|
|
29
|
+
this.onMenuItemClick = onMenuItemClick;
|
|
30
|
+
this.data = {};
|
|
31
|
+
this.clickFns = [];
|
|
32
|
+
this.selector = params.selector;
|
|
33
|
+
this.hasTrailingDivider = params.hasTrailingDivider;
|
|
34
|
+
this.show = typeof params.show === 'undefined' || params.show;
|
|
35
|
+
this.coreAsWell = params.coreAsWell || false;
|
|
36
|
+
this.scratchpad = scratchpad;
|
|
37
|
+
if (typeof params.onClickFunction === 'undefined' &&
|
|
38
|
+
typeof params.submenu === 'undefined') {
|
|
39
|
+
throw new Error('A menu item must either have click function or a submenu or both');
|
|
40
|
+
}
|
|
41
|
+
this.onClickFunction = params.onClickFunction;
|
|
42
|
+
if (params.submenu instanceof Array) {
|
|
43
|
+
this._createSubmenu(params.submenu);
|
|
44
|
+
}
|
|
45
|
+
super.addEventListener('mousedown', stopEvent);
|
|
46
|
+
super.addEventListener('mouseup', stopEvent);
|
|
47
|
+
super.addEventListener('touchstart', stopEvent);
|
|
48
|
+
super.addEventListener('touchend', stopEvent);
|
|
49
|
+
}
|
|
50
|
+
bindOnClickFunction(onClickFn) {
|
|
51
|
+
this.clickFns.push(onClickFn);
|
|
52
|
+
super.addEventListener('click', onClickFn);
|
|
53
|
+
}
|
|
54
|
+
unbindOnClickFunctions() {
|
|
55
|
+
for (const onClickFn of this.clickFns) {
|
|
56
|
+
super.removeEventListener('click', onClickFn);
|
|
57
|
+
}
|
|
58
|
+
this.clickFns = [];
|
|
59
|
+
}
|
|
60
|
+
enable() {
|
|
61
|
+
setBooleanAttribute(this, 'disabled', false);
|
|
62
|
+
if (this.hasSubmenu() && this.mouseEnterHandler) {
|
|
63
|
+
this.addEventListener('mouseenter', this.mouseEnterHandler);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
disable() {
|
|
67
|
+
setBooleanAttribute(this, 'disabled', true);
|
|
68
|
+
if (this.hasSubmenu() && this.mouseEnterHandler) {
|
|
69
|
+
this.removeEventListener('mouseenter', this.mouseEnterHandler);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
hide() {
|
|
73
|
+
this.show = false;
|
|
74
|
+
this.style.display = 'none';
|
|
75
|
+
}
|
|
76
|
+
getHasTrailingDivider() {
|
|
77
|
+
return this.hasTrailingDivider ? true : false;
|
|
78
|
+
}
|
|
79
|
+
setHasTrailingDivider(status) {
|
|
80
|
+
this.hasTrailingDivider = status;
|
|
81
|
+
}
|
|
82
|
+
hasSubmenu() {
|
|
83
|
+
return this.submenu instanceof MenuItemList;
|
|
84
|
+
}
|
|
85
|
+
appendSubmenuItem(menuItem, before) {
|
|
86
|
+
if (!this.hasSubmenu()) {
|
|
87
|
+
this._createSubmenu();
|
|
88
|
+
}
|
|
89
|
+
this.submenu.appendMenuItem(menuItem, before);
|
|
90
|
+
}
|
|
91
|
+
isClickable() {
|
|
92
|
+
return this.onClickFunction !== undefined;
|
|
93
|
+
}
|
|
94
|
+
display() {
|
|
95
|
+
this.show = true;
|
|
96
|
+
this.style.display = 'block';
|
|
97
|
+
}
|
|
98
|
+
isVisible() {
|
|
99
|
+
return this.show === true && this.style.display !== 'none';
|
|
100
|
+
}
|
|
101
|
+
removeSubmenu() {
|
|
102
|
+
if (this.hasSubmenu()) {
|
|
103
|
+
this.submenu.removeAllMenuItems();
|
|
104
|
+
this.detachSubmenu();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
detachSubmenu() {
|
|
108
|
+
if (this.hasSubmenu()) {
|
|
109
|
+
this.removeChild(this.submenu);
|
|
110
|
+
this.removeChild(this.indicator);
|
|
111
|
+
this.removeEventListener('mouseenter', this.mouseEnterHandler);
|
|
112
|
+
this.removeEventListener('mouseleave', this.mouseLeaveHandler);
|
|
113
|
+
this.submenu = undefined;
|
|
114
|
+
this.indicator = undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
_onMouseEnter(_event) {
|
|
118
|
+
const rect = this.getBoundingClientRect();
|
|
119
|
+
const submenuRect = getDimensionsHidden(this.submenu);
|
|
120
|
+
const exceedsRight = rect.right + submenuRect.width > window.innerWidth;
|
|
121
|
+
const exceedsBottom = rect.top + submenuRect.height > window.innerHeight;
|
|
122
|
+
if (!exceedsRight && !exceedsBottom) {
|
|
123
|
+
this.submenu.style.left = this.clientWidth + 'px';
|
|
124
|
+
this.submenu.style.top = '0px';
|
|
125
|
+
this.submenu.style.right = 'auto';
|
|
126
|
+
this.submenu.style.bottom = 'auto';
|
|
127
|
+
}
|
|
128
|
+
else if (exceedsRight && !exceedsBottom) {
|
|
129
|
+
this.submenu.style.right = this.clientWidth + 'px';
|
|
130
|
+
this.submenu.style.top = '0px';
|
|
131
|
+
this.submenu.style.left = 'auto';
|
|
132
|
+
this.submenu.style.bottom = 'auto';
|
|
133
|
+
}
|
|
134
|
+
else if (exceedsRight && exceedsBottom) {
|
|
135
|
+
this.submenu.style.right = this.clientWidth + 'px';
|
|
136
|
+
this.submenu.style.bottom = '0px';
|
|
137
|
+
this.submenu.style.top = 'auto';
|
|
138
|
+
this.submenu.style.left = 'auto';
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
this.submenu.style.left = this.clientWidth + 'px';
|
|
142
|
+
this.submenu.style.bottom = '0px';
|
|
143
|
+
this.submenu.style.right = 'auto';
|
|
144
|
+
this.submenu.style.top = 'auto';
|
|
145
|
+
}
|
|
146
|
+
this.submenu.display();
|
|
147
|
+
const visibleItems = Array.from(this.submenu.children).filter((item) => {
|
|
148
|
+
if (item instanceof MenuItem)
|
|
149
|
+
return item.isVisible();
|
|
150
|
+
return false;
|
|
151
|
+
});
|
|
152
|
+
const length = visibleItems.length;
|
|
153
|
+
visibleItems.forEach((item, index) => {
|
|
154
|
+
if (!(item instanceof MenuItem))
|
|
155
|
+
return;
|
|
156
|
+
if (index < length - 1 && item.getHasTrailingDivider()) {
|
|
157
|
+
item.classList.add(DIVIDER_CSS_CLASS);
|
|
158
|
+
}
|
|
159
|
+
else if (item.getHasTrailingDivider()) {
|
|
160
|
+
item.classList.remove(DIVIDER_CSS_CLASS);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
_onMouseLeave(event) {
|
|
165
|
+
const pos = { x: event.clientX, y: event.clientY };
|
|
166
|
+
if (!isIn(pos, this.submenu)) {
|
|
167
|
+
this.submenu.hide();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
_createSubmenu(items = []) {
|
|
171
|
+
this.indicator = this.scratchpad['submenuIndicatorGen']();
|
|
172
|
+
this.submenu = new MenuItemList(this.onMenuItemClick, this.scratchpad);
|
|
173
|
+
this.appendChild(this.indicator);
|
|
174
|
+
this.appendChild(this.submenu);
|
|
175
|
+
for (const item of items) {
|
|
176
|
+
const menuItem = new MenuItem(item, this.onMenuItemClick, this.scratchpad);
|
|
177
|
+
this.submenu.appendMenuItem(menuItem);
|
|
178
|
+
}
|
|
179
|
+
this.mouseEnterHandler = this._onMouseEnter.bind(this);
|
|
180
|
+
this.mouseLeaveHandler = this._onMouseLeave.bind(this);
|
|
181
|
+
this.addEventListener('mouseenter', this.mouseEnterHandler);
|
|
182
|
+
this.addEventListener('mouseleave', this.mouseLeaveHandler);
|
|
183
|
+
}
|
|
184
|
+
_getMenuItemClassStr(classStr, hasTrailingDivider) {
|
|
185
|
+
return hasTrailingDivider
|
|
186
|
+
? classStr + ' ' + DIVIDER_CSS_CLASS
|
|
187
|
+
: classStr;
|
|
188
|
+
}
|
|
189
|
+
static define() {
|
|
190
|
+
defineCustomElement('ctx-menu-item', MenuItem, 'button');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export class MenuItemList extends HTMLDivElement {
|
|
194
|
+
constructor(onMenuItemClick, scratchpad) {
|
|
195
|
+
super();
|
|
196
|
+
super.setAttribute('class', scratchpad['cxtMenuClasses']);
|
|
197
|
+
this.style.position = 'absolute';
|
|
198
|
+
this.onMenuItemClick = onMenuItemClick;
|
|
199
|
+
this.scratchpad = scratchpad;
|
|
200
|
+
}
|
|
201
|
+
hide() {
|
|
202
|
+
if (this.isVisible()) {
|
|
203
|
+
this.hideSubmenus();
|
|
204
|
+
this.style.display = 'none';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
display() {
|
|
208
|
+
this.style.display = 'block';
|
|
209
|
+
}
|
|
210
|
+
isVisible() {
|
|
211
|
+
return this.style.display !== 'none';
|
|
212
|
+
}
|
|
213
|
+
hideMenuItems() {
|
|
214
|
+
for (const item of this.children) {
|
|
215
|
+
if (item instanceof HTMLElement) {
|
|
216
|
+
item.style.display = 'none';
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.warn(`${item} is not a HTMLElement`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
hideSubmenus() {
|
|
224
|
+
for (const menuItem of this.children) {
|
|
225
|
+
if (menuItem instanceof MenuItem) {
|
|
226
|
+
if (menuItem.submenu) {
|
|
227
|
+
menuItem.submenu.hide();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
appendMenuItem(menuItem, before) {
|
|
233
|
+
if (typeof before !== 'undefined') {
|
|
234
|
+
if (before.parentNode === this) {
|
|
235
|
+
this.insertBefore(menuItem, before);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
throw new Error(`The item with id='${before.id}' is not a child of the context menu`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
this.appendChild(menuItem);
|
|
243
|
+
}
|
|
244
|
+
if (menuItem.isClickable()) {
|
|
245
|
+
this._performBindings(menuItem);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
moveBefore(menuItem, before) {
|
|
249
|
+
if (menuItem.parentNode !== this) {
|
|
250
|
+
throw new Error(`The item with id='${menuItem.id}' is not a child of context menu`);
|
|
251
|
+
}
|
|
252
|
+
if (before.parentNode !== this) {
|
|
253
|
+
throw new Error(`The item with id='${before.id}' is not a child of context menu`);
|
|
254
|
+
}
|
|
255
|
+
this.removeChild(menuItem);
|
|
256
|
+
this.insertBefore(menuItem, before);
|
|
257
|
+
}
|
|
258
|
+
removeAllMenuItems() {
|
|
259
|
+
while (this.firstChild) {
|
|
260
|
+
const child = this.lastChild;
|
|
261
|
+
if (child instanceof MenuItem) {
|
|
262
|
+
this._removeImmediateMenuItem(child);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
console.warn('Found non menu item in the context menu: ', child);
|
|
266
|
+
this.removeChild(child);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
_removeImmediateMenuItem(menuItem) {
|
|
271
|
+
if (this._detachImmediateMenuItem(menuItem)) {
|
|
272
|
+
menuItem.detachSubmenu();
|
|
273
|
+
menuItem.unbindOnClickFunctions();
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
throw new Error(`menu item(id=${menuItem.id}) is not in the context menu`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
_detachImmediateMenuItem(menuItem) {
|
|
280
|
+
if (menuItem.parentNode === this) {
|
|
281
|
+
this.removeChild(menuItem);
|
|
282
|
+
if (this.children.length <= 0) {
|
|
283
|
+
const parent = this.parentNode;
|
|
284
|
+
if (parent instanceof MenuItem) {
|
|
285
|
+
parent.detachSubmenu();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
_performBindings(menuItem) {
|
|
293
|
+
const callback = this._bindOnClick(menuItem.onClickFunction);
|
|
294
|
+
menuItem.bindOnClickFunction(callback);
|
|
295
|
+
menuItem.bindOnClickFunction(this.onMenuItemClick);
|
|
296
|
+
}
|
|
297
|
+
_bindOnClick(onClickFn) {
|
|
298
|
+
return () => {
|
|
299
|
+
const event = this.scratchpad['currentCyEvent'];
|
|
300
|
+
onClickFn(event);
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
static define() {
|
|
304
|
+
defineCustomElement('menu-item-list', MenuItemList, 'div');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
export class ContextMenu extends MenuItemList {
|
|
308
|
+
constructor(onMenuItemClick, scratchpad) {
|
|
309
|
+
super(onMenuItemClick, scratchpad);
|
|
310
|
+
this.onMenuItemClick = (event) => {
|
|
311
|
+
stopEvent(event);
|
|
312
|
+
this.hide();
|
|
313
|
+
onMenuItemClick();
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
removeMenuItem(menuItem) {
|
|
317
|
+
const parent = menuItem.parentElement;
|
|
318
|
+
if (parent instanceof MenuItemList && this.contains(parent)) {
|
|
319
|
+
parent._removeImmediateMenuItem(menuItem);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
appendMenuItem(menuItem, before) {
|
|
323
|
+
this.ensureDoesntContain(menuItem.id);
|
|
324
|
+
super.appendMenuItem(menuItem, before);
|
|
325
|
+
}
|
|
326
|
+
insertMenuItem(menuItem, { before, parent } = {}) {
|
|
327
|
+
this.ensureDoesntContain(menuItem.id);
|
|
328
|
+
if (typeof before !== 'undefined') {
|
|
329
|
+
if (this.contains(before)) {
|
|
330
|
+
const parentNode = before.parentNode;
|
|
331
|
+
if (parentNode instanceof MenuItemList) {
|
|
332
|
+
parentNode.appendMenuItem(menuItem, before);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
throw new Error(`Parent of before(id=${before.id}) is not a submenu`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
throw new Error(`before(id=${before.id}) is not in the context menu`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else if (typeof parent !== 'undefined') {
|
|
343
|
+
if (this.contains(parent)) {
|
|
344
|
+
parent.appendSubmenuItem(menuItem);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
throw new Error(`parent(id=${parent.id}) is not a descendant of the context menu`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
this.appendMenuItem(menuItem);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
moveBefore(menuItem, before) {
|
|
355
|
+
const parent = menuItem.parentElement;
|
|
356
|
+
if (this.contains(parent)) {
|
|
357
|
+
if (this.contains(before)) {
|
|
358
|
+
parent.removeChild(menuItem);
|
|
359
|
+
this.insertMenuItem(menuItem, { before });
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
throw new Error(`before(id=${before.id}) is not in the context menu`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
throw new Error(`parent(id=${parent?.id}) is not in the context menu`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
moveToSubmenu(menuItem, parent = null, options = null) {
|
|
370
|
+
const oldParent = menuItem.parentElement;
|
|
371
|
+
if (oldParent instanceof MenuItemList) {
|
|
372
|
+
if (this.contains(oldParent)) {
|
|
373
|
+
if (parent !== null) {
|
|
374
|
+
if (this.contains(parent)) {
|
|
375
|
+
oldParent._detachImmediateMenuItem(menuItem);
|
|
376
|
+
parent.appendSubmenuItem(menuItem);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
throw new Error(`parent(id=${parent.id}) is not in the context menu`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
if (options !== null) {
|
|
384
|
+
menuItem.selector = options.selector ?? menuItem.selector;
|
|
385
|
+
menuItem.coreAsWell = options.coreAsWell ?? menuItem.coreAsWell;
|
|
386
|
+
}
|
|
387
|
+
oldParent._detachImmediateMenuItem(menuItem);
|
|
388
|
+
this.appendMenuItem(menuItem);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
throw new Error(`parent of the menu item(id=${oldParent.id}) is not in the context menu`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
throw new Error(`current parent(id=${oldParent?.id}) is not a submenu`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
ensureDoesntContain(id) {
|
|
400
|
+
const elem = document.getElementById(id);
|
|
401
|
+
if (typeof elem !== 'undefined' && elem && this.contains(elem)) {
|
|
402
|
+
throw new Error(`There is already an element with id=${id} in the context menu`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
static define() {
|
|
406
|
+
defineCustomElement('ctx-menu', ContextMenu, 'div');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Core } from 'cytoscape';
|
|
2
|
+
import type { CytoscapeContextMenuOptions, MenuItemOption } from './constants';
|
|
3
|
+
export interface ContextMenusInstance {
|
|
4
|
+
isActive: () => boolean;
|
|
5
|
+
appendMenuItem: (item: MenuItemOption, parentID?: string) => Core;
|
|
6
|
+
appendMenuItems: (items: MenuItemOption[], parentID?: string) => Core;
|
|
7
|
+
removeMenuItem: (itemID: string) => Core;
|
|
8
|
+
hide: () => void;
|
|
9
|
+
setTrailingDivider: (itemID: string, status: boolean) => Core;
|
|
10
|
+
insertBeforeMenuItem: (item: MenuItemOption, existingItemID: string) => Core;
|
|
11
|
+
moveToSubmenu: (itemID: string, options?: string | {
|
|
12
|
+
coreAsWell?: boolean;
|
|
13
|
+
selector?: string;
|
|
14
|
+
}) => Core;
|
|
15
|
+
moveBeforeOtherMenuItem: (itemID: string, existingItemID: string) => Core;
|
|
16
|
+
disableMenuItem: (itemID: string) => Core;
|
|
17
|
+
enableMenuItem: (itemID: string) => Core;
|
|
18
|
+
hideMenuItem: (itemID: string) => Core;
|
|
19
|
+
showMenuItem: (itemID: string) => Core;
|
|
20
|
+
destroy: () => Core;
|
|
21
|
+
}
|
|
22
|
+
export declare function contextMenus(this: Core, opts: CytoscapeContextMenuOptions | 'get'): ContextMenusInstance;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import * as utils from './utils';
|
|
2
|
+
import { DEFAULT_OPTS, DIVIDER_CSS_CLASS, INDICATOR_CSS_CLASS, } from './constants';
|
|
3
|
+
import { MenuItem, ContextMenu, MenuItemList } from './context-menu';
|
|
4
|
+
export function contextMenus(opts) {
|
|
5
|
+
const cy = this;
|
|
6
|
+
if (!cy.scratch('cycontextmenus')) {
|
|
7
|
+
cy.scratch('cycontextmenus', {});
|
|
8
|
+
}
|
|
9
|
+
const getScratchProp = (propname) => cy.scratch('cycontextmenus')[propname];
|
|
10
|
+
const setScratchProp = (propname, value) => (cy.scratch('cycontextmenus')[propname] = value);
|
|
11
|
+
let cxtMenu = getScratchProp('cxtMenu');
|
|
12
|
+
let options = getScratchProp('options');
|
|
13
|
+
const getVariation = (position) => {
|
|
14
|
+
if (typeof position === 'undefined')
|
|
15
|
+
return { x: 0, y: 0 };
|
|
16
|
+
return { x: -200, y: 50 };
|
|
17
|
+
};
|
|
18
|
+
const addPositionToEvent = (event, position) => {
|
|
19
|
+
if (typeof position === 'undefined')
|
|
20
|
+
return;
|
|
21
|
+
const cyPos = event.position || event.cyPosition;
|
|
22
|
+
if (typeof cyPos === 'undefined') {
|
|
23
|
+
event.position = position;
|
|
24
|
+
}
|
|
25
|
+
const renderedPos = event.renderedPosition ||
|
|
26
|
+
event.cyRenderedPosition;
|
|
27
|
+
if (typeof renderedPos === 'undefined') {
|
|
28
|
+
event.renderedPosition = position;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const bindOnCxttap = () => {
|
|
32
|
+
const onCxttap = (event, position, someNode) => {
|
|
33
|
+
if (typeof someNode !== 'undefined') {
|
|
34
|
+
event.data = { node: someNode };
|
|
35
|
+
}
|
|
36
|
+
setScratchProp('currentCyEvent', event);
|
|
37
|
+
adjustCxtMenu(event, position);
|
|
38
|
+
const target = event.target || event.cyTarget;
|
|
39
|
+
for (const menuItem of cxtMenu.children) {
|
|
40
|
+
if (menuItem instanceof MenuItem) {
|
|
41
|
+
const shouldDisplay = target === cy
|
|
42
|
+
? menuItem.coreAsWell
|
|
43
|
+
: target.is(menuItem.selector);
|
|
44
|
+
if (shouldDisplay && menuItem.show) {
|
|
45
|
+
cxtMenu.display();
|
|
46
|
+
setScratchProp('anyVisibleChild', true);
|
|
47
|
+
menuItem.display();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const visibleItems = Array.from(cxtMenu.children).filter((item) => {
|
|
52
|
+
if (item instanceof MenuItem)
|
|
53
|
+
return item.isVisible();
|
|
54
|
+
return false;
|
|
55
|
+
});
|
|
56
|
+
const length = visibleItems.length;
|
|
57
|
+
visibleItems.forEach((item, index) => {
|
|
58
|
+
if (!(item instanceof MenuItem))
|
|
59
|
+
return;
|
|
60
|
+
if (index < length - 1 && item.getHasTrailingDivider()) {
|
|
61
|
+
item.classList.add(DIVIDER_CSS_CLASS);
|
|
62
|
+
}
|
|
63
|
+
else if (item.getHasTrailingDivider()) {
|
|
64
|
+
item.classList.remove(DIVIDER_CSS_CLASS);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (!getScratchProp('anyVisibleChild') &&
|
|
68
|
+
utils.isElementVisible(cxtMenu)) {
|
|
69
|
+
cxtMenu.hide();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
cy.on(options.evtType, onCxttap);
|
|
73
|
+
setScratchProp('onCxttap', onCxttap);
|
|
74
|
+
};
|
|
75
|
+
const bindCyEvents = () => {
|
|
76
|
+
const eventCyTapStart = (event) => {
|
|
77
|
+
if (cxtMenu.contains(event.originalEvent.target)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
cxtMenu.hide();
|
|
81
|
+
setScratchProp('cxtMenuPosition', undefined);
|
|
82
|
+
setScratchProp('currentCyEvent', undefined);
|
|
83
|
+
};
|
|
84
|
+
cy.on('tapstart', eventCyTapStart);
|
|
85
|
+
setScratchProp('eventCyTapStart', eventCyTapStart);
|
|
86
|
+
const eventCyViewport = () => {
|
|
87
|
+
cxtMenu.hide();
|
|
88
|
+
};
|
|
89
|
+
cy.on('viewport', eventCyViewport);
|
|
90
|
+
setScratchProp('onViewport', eventCyViewport);
|
|
91
|
+
};
|
|
92
|
+
const bindHideCallbacks = () => {
|
|
93
|
+
const onClick = (event) => {
|
|
94
|
+
const cyContainer = cy.container();
|
|
95
|
+
if (!cyContainer.contains(event.target) &&
|
|
96
|
+
!cxtMenu.contains(event.target)) {
|
|
97
|
+
cxtMenu.hide();
|
|
98
|
+
setScratchProp('cxtMenuPosition', undefined);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
document.addEventListener('mouseup', onClick);
|
|
102
|
+
setScratchProp('hideOnNonCyClick', onClick);
|
|
103
|
+
};
|
|
104
|
+
const adjustCxtMenu = (event, position) => {
|
|
105
|
+
const container = cy.container();
|
|
106
|
+
const currentCxtMenuPosition = getScratchProp('cxtMenuPosition');
|
|
107
|
+
addPositionToEvent(event, position);
|
|
108
|
+
const cyPos = event.position ||
|
|
109
|
+
event.cyPosition;
|
|
110
|
+
if (currentCxtMenuPosition != cyPos) {
|
|
111
|
+
cxtMenu.hideMenuItems();
|
|
112
|
+
setScratchProp('anyVisibleChild', false);
|
|
113
|
+
setScratchProp('cxtMenuPosition', cyPos);
|
|
114
|
+
const containerPos = utils.getOffset(container);
|
|
115
|
+
const renderedPos = event.renderedPosition ||
|
|
116
|
+
event.cyRenderedPosition;
|
|
117
|
+
const borderWidth = getComputedStyle(container).getPropertyValue('border-width');
|
|
118
|
+
const borderThickness = parseInt(borderWidth.replace('px', ''), 10) || 0;
|
|
119
|
+
if (borderThickness > 0) {
|
|
120
|
+
containerPos.top += borderThickness;
|
|
121
|
+
containerPos.left += borderThickness;
|
|
122
|
+
}
|
|
123
|
+
const containerHeight = container.clientHeight;
|
|
124
|
+
const containerWidth = container.clientWidth;
|
|
125
|
+
const horizontalSplit = containerHeight / 2;
|
|
126
|
+
const verticalSplit = containerWidth / 2;
|
|
127
|
+
const variation = getVariation(position);
|
|
128
|
+
if (renderedPos.y > horizontalSplit && renderedPos.x <= verticalSplit) {
|
|
129
|
+
cxtMenu.style.left = renderedPos.x - variation.x + 'px';
|
|
130
|
+
cxtMenu.style.bottom =
|
|
131
|
+
containerHeight - renderedPos.y + variation.y + 'px';
|
|
132
|
+
cxtMenu.style.right = 'auto';
|
|
133
|
+
cxtMenu.style.top = 'auto';
|
|
134
|
+
}
|
|
135
|
+
else if (renderedPos.y > horizontalSplit &&
|
|
136
|
+
renderedPos.x > verticalSplit) {
|
|
137
|
+
cxtMenu.style.right =
|
|
138
|
+
containerWidth - renderedPos.x - variation.x + 'px';
|
|
139
|
+
cxtMenu.style.bottom =
|
|
140
|
+
containerHeight - renderedPos.y + variation.y + 'px';
|
|
141
|
+
cxtMenu.style.left = 'auto';
|
|
142
|
+
cxtMenu.style.top = 'auto';
|
|
143
|
+
}
|
|
144
|
+
else if (renderedPos.y <= horizontalSplit &&
|
|
145
|
+
renderedPos.x <= verticalSplit) {
|
|
146
|
+
cxtMenu.style.left = renderedPos.x - variation.x + 'px';
|
|
147
|
+
cxtMenu.style.top = renderedPos.y + variation.y + 'px';
|
|
148
|
+
cxtMenu.style.right = 'auto';
|
|
149
|
+
cxtMenu.style.bottom = 'auto';
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
cxtMenu.style.right =
|
|
153
|
+
containerWidth - renderedPos.x - variation.x + 'px';
|
|
154
|
+
cxtMenu.style.top = renderedPos.y + 'px';
|
|
155
|
+
cxtMenu.style.left = 'auto';
|
|
156
|
+
cxtMenu.style.bottom = 'auto';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const createAndAppendMenuItemComponent = (opts, parentID) => {
|
|
161
|
+
const menuItemComponent = createMenuItemComponent(opts);
|
|
162
|
+
if (typeof parentID !== 'undefined') {
|
|
163
|
+
const parent = asMenuItem(parentID);
|
|
164
|
+
cxtMenu.insertMenuItem(menuItemComponent, { parent });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
cxtMenu.insertMenuItem(menuItemComponent);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const createAndAppendMenuItemComponents = (optionsArr, parentID) => {
|
|
171
|
+
for (let i = 0; i < optionsArr.length; i++) {
|
|
172
|
+
createAndAppendMenuItemComponent(optionsArr[i], parentID);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const createMenuItemComponent = (opts) => {
|
|
176
|
+
const scratchpad = cy.scratch('cycontextmenus');
|
|
177
|
+
return new MenuItem(opts, cxtMenu.onMenuItemClick, scratchpad);
|
|
178
|
+
};
|
|
179
|
+
const destroyCxtMenu = () => {
|
|
180
|
+
if (!getScratchProp('active')) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
cxtMenu.removeAllMenuItems();
|
|
184
|
+
cy.off('tapstart', getScratchProp('eventCyTapStart'));
|
|
185
|
+
cy.off(options.evtType, getScratchProp('onCxttap'));
|
|
186
|
+
cy.off('viewport', getScratchProp('onViewport'));
|
|
187
|
+
document.removeEventListener('mouseup', getScratchProp('hideOnNonCyClick'));
|
|
188
|
+
cxtMenu.parentNode.removeChild(cxtMenu);
|
|
189
|
+
cxtMenu = undefined;
|
|
190
|
+
setScratchProp('cxtMenu', undefined);
|
|
191
|
+
setScratchProp('active', false);
|
|
192
|
+
setScratchProp('anyVisibleChild', false);
|
|
193
|
+
setScratchProp('onCxttap', undefined);
|
|
194
|
+
setScratchProp('onViewport', undefined);
|
|
195
|
+
setScratchProp('hideOnNonCyClick', undefined);
|
|
196
|
+
};
|
|
197
|
+
const makeSubmenuIndicator = (props) => {
|
|
198
|
+
const elem = document.createElement('img');
|
|
199
|
+
elem.src = props.src;
|
|
200
|
+
elem.width = props.width;
|
|
201
|
+
elem.height = props.height;
|
|
202
|
+
elem.classList.add(INDICATOR_CSS_CLASS);
|
|
203
|
+
return elem;
|
|
204
|
+
};
|
|
205
|
+
const asMenuItem = (menuItemID) => {
|
|
206
|
+
const menuItem = document.getElementById(menuItemID);
|
|
207
|
+
if (menuItem instanceof MenuItem) {
|
|
208
|
+
return menuItem;
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`The item with id=${menuItemID} is not a menu item`);
|
|
211
|
+
};
|
|
212
|
+
const getInstance = () => {
|
|
213
|
+
const getCxtMenu = () => getScratchProp('cxtMenu');
|
|
214
|
+
return {
|
|
215
|
+
isActive: () => getScratchProp('active'),
|
|
216
|
+
appendMenuItem: (item, parentID) => {
|
|
217
|
+
createAndAppendMenuItemComponent(item, parentID);
|
|
218
|
+
return cy;
|
|
219
|
+
},
|
|
220
|
+
appendMenuItems: (items, parentID) => {
|
|
221
|
+
createAndAppendMenuItemComponents(items, parentID);
|
|
222
|
+
return cy;
|
|
223
|
+
},
|
|
224
|
+
removeMenuItem: (itemID) => {
|
|
225
|
+
const item = asMenuItem(itemID);
|
|
226
|
+
getCxtMenu()?.removeMenuItem(item);
|
|
227
|
+
return cy;
|
|
228
|
+
},
|
|
229
|
+
hide: () => {
|
|
230
|
+
getCxtMenu()?.hide();
|
|
231
|
+
},
|
|
232
|
+
setTrailingDivider: (itemID, status) => {
|
|
233
|
+
const menuItem = asMenuItem(itemID);
|
|
234
|
+
menuItem.setHasTrailingDivider(status);
|
|
235
|
+
if (status) {
|
|
236
|
+
menuItem.classList.add(DIVIDER_CSS_CLASS);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
menuItem.classList.remove(DIVIDER_CSS_CLASS);
|
|
240
|
+
}
|
|
241
|
+
return cy;
|
|
242
|
+
},
|
|
243
|
+
insertBeforeMenuItem: (item, existingItemID) => {
|
|
244
|
+
const menuItemComponent = createMenuItemComponent(item);
|
|
245
|
+
const existingItem = asMenuItem(existingItemID);
|
|
246
|
+
getCxtMenu()?.insertMenuItem(menuItemComponent, { before: existingItem });
|
|
247
|
+
return cy;
|
|
248
|
+
},
|
|
249
|
+
moveToSubmenu: (itemID, optionsParam) => {
|
|
250
|
+
const item = asMenuItem(itemID);
|
|
251
|
+
const menu = getCxtMenu();
|
|
252
|
+
if (optionsParam === undefined || optionsParam === null) {
|
|
253
|
+
menu?.moveToSubmenu(item);
|
|
254
|
+
}
|
|
255
|
+
else if (typeof optionsParam === 'string') {
|
|
256
|
+
const parent = asMenuItem(optionsParam);
|
|
257
|
+
menu?.moveToSubmenu(item, parent);
|
|
258
|
+
}
|
|
259
|
+
else if (typeof optionsParam.coreAsWell !==
|
|
260
|
+
'undefined' ||
|
|
261
|
+
typeof optionsParam.selector !== 'undefined') {
|
|
262
|
+
menu?.moveToSubmenu(item, null, optionsParam);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
console.warn('options neither has coreAsWell nor selector property but it is an object. Are you sure that this is what you want to do?');
|
|
266
|
+
}
|
|
267
|
+
return cy;
|
|
268
|
+
},
|
|
269
|
+
moveBeforeOtherMenuItem: (itemID, existingItemID) => {
|
|
270
|
+
const item = asMenuItem(itemID);
|
|
271
|
+
const before = asMenuItem(existingItemID);
|
|
272
|
+
getCxtMenu()?.moveBefore(item, before);
|
|
273
|
+
return cy;
|
|
274
|
+
},
|
|
275
|
+
disableMenuItem: (itemID) => {
|
|
276
|
+
const menuItem = asMenuItem(itemID);
|
|
277
|
+
menuItem.disable();
|
|
278
|
+
return cy;
|
|
279
|
+
},
|
|
280
|
+
enableMenuItem: (itemID) => {
|
|
281
|
+
const menuItem = asMenuItem(itemID);
|
|
282
|
+
menuItem.enable();
|
|
283
|
+
return cy;
|
|
284
|
+
},
|
|
285
|
+
hideMenuItem: (itemID) => {
|
|
286
|
+
const menuItem = asMenuItem(itemID);
|
|
287
|
+
menuItem.hide();
|
|
288
|
+
return cy;
|
|
289
|
+
},
|
|
290
|
+
showMenuItem: (itemID) => {
|
|
291
|
+
const menuItem = asMenuItem(itemID);
|
|
292
|
+
menuItem.display();
|
|
293
|
+
return cy;
|
|
294
|
+
},
|
|
295
|
+
destroy: () => {
|
|
296
|
+
destroyCxtMenu();
|
|
297
|
+
return cy;
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
if (opts !== 'get') {
|
|
302
|
+
MenuItem.define();
|
|
303
|
+
MenuItemList.define();
|
|
304
|
+
ContextMenu.define();
|
|
305
|
+
options = utils.extend(DEFAULT_OPTS, opts);
|
|
306
|
+
setScratchProp('options', options);
|
|
307
|
+
if (getScratchProp('active')) {
|
|
308
|
+
destroyCxtMenu();
|
|
309
|
+
}
|
|
310
|
+
setScratchProp('active', true);
|
|
311
|
+
setScratchProp('submenuIndicatorGen', makeSubmenuIndicator.bind(undefined, options.submenuIndicator || DEFAULT_OPTS.submenuIndicator));
|
|
312
|
+
const cxtMenuClasses = utils.getClassStr(options.contextMenuClasses || DEFAULT_OPTS.contextMenuClasses);
|
|
313
|
+
setScratchProp('cxtMenuClasses', cxtMenuClasses);
|
|
314
|
+
const onMenuItemClick = () => setScratchProp('cxtMenuPosition', undefined);
|
|
315
|
+
const scratchpad = cy.scratch('cycontextmenus');
|
|
316
|
+
cxtMenu = new ContextMenu(onMenuItemClick, scratchpad);
|
|
317
|
+
setScratchProp('cxtMenu', cxtMenu);
|
|
318
|
+
cy.container().appendChild(cxtMenu);
|
|
319
|
+
setScratchProp('cxtMenuItemClasses', utils.getClassStr(options.menuItemClasses || DEFAULT_OPTS.menuItemClasses));
|
|
320
|
+
const menuItems = options.menuItems || [];
|
|
321
|
+
createAndAppendMenuItemComponents(menuItems);
|
|
322
|
+
bindOnCxttap();
|
|
323
|
+
bindCyEvents();
|
|
324
|
+
bindHideCallbacks();
|
|
325
|
+
utils.preventDefaultContextTap();
|
|
326
|
+
}
|
|
327
|
+
return getInstance();
|
|
328
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import cytoscape from 'cytoscape';
|
|
2
|
+
import { contextMenus } from './cytoscape-context-menus';
|
|
3
|
+
export type { CytoscapeContextMenuOptions, MenuItemOption } from './constants';
|
|
4
|
+
export type { ContextMenusInstance } from './cytoscape-context-menus';
|
|
5
|
+
export { contextMenus };
|
|
6
|
+
export declare function registerCytoscapeContextMenus(cy?: typeof cytoscape): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import cytoscape from 'cytoscape';
|
|
2
|
+
import { contextMenus } from './cytoscape-context-menus';
|
|
3
|
+
export { contextMenus };
|
|
4
|
+
export function registerCytoscapeContextMenus(cy) {
|
|
5
|
+
const target = cy ?? (typeof cytoscape !== 'undefined' ? cytoscape : undefined);
|
|
6
|
+
if (!target) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
target('core', 'contextMenus', contextMenus);
|
|
10
|
+
}
|
|
11
|
+
if (typeof cytoscape !== 'undefined') {
|
|
12
|
+
registerCytoscapeContextMenus(cytoscape);
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.cy-context-menus-cxt-menu {
|
|
2
|
+
position: absolute;
|
|
3
|
+
z-index: 1000;
|
|
4
|
+
display: none;
|
|
5
|
+
min-width: 120px;
|
|
6
|
+
padding: 4px 0;
|
|
7
|
+
background: #fff;
|
|
8
|
+
border: 1px solid #ccc;
|
|
9
|
+
border-radius: 4px;
|
|
10
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
11
|
+
font-family: inherit;
|
|
12
|
+
font-size: 14px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.cy-context-menus-cxt-menuitem {
|
|
16
|
+
display: block;
|
|
17
|
+
width: 100%;
|
|
18
|
+
padding: 8px 24px 8px 12px;
|
|
19
|
+
border: none;
|
|
20
|
+
background: transparent;
|
|
21
|
+
text-align: left;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
white-space: nowrap;
|
|
24
|
+
|
|
25
|
+
&:hover:not(:disabled) {
|
|
26
|
+
background: #f0f0f0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&:disabled {
|
|
30
|
+
opacity: 0.5;
|
|
31
|
+
cursor: not-allowed;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.cy-context-menus-divider {
|
|
36
|
+
border-bottom: 1px solid #eee;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.cy-context-menus-submenu-indicator {
|
|
40
|
+
position: absolute;
|
|
41
|
+
right: 8px;
|
|
42
|
+
top: 50%;
|
|
43
|
+
transform: translateY(-50%);
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function getOffset(el: Element): {
|
|
2
|
+
top: number;
|
|
3
|
+
left: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function matches(el: Element, selector: string): boolean;
|
|
6
|
+
export declare function isElementHidden(elem: HTMLElement): boolean;
|
|
7
|
+
export declare function isElementVisible(elem: HTMLElement): boolean;
|
|
8
|
+
export declare function extend<T extends Record<string, unknown>>(defaults: T, options: Partial<T>): T;
|
|
9
|
+
export declare function getClassStr(classes: string[]): string;
|
|
10
|
+
export declare function preventDefaultContextTap(): void;
|
|
11
|
+
export declare function setBooleanAttribute(element: Element, attribute: string, boolValue: boolean): void;
|
|
12
|
+
export declare function isIn({ x, y }: {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
}, element: Element): boolean;
|
|
16
|
+
export declare function getDimensionsHidden(element: HTMLElement): DOMRect;
|
|
17
|
+
export declare function defineCustomElement(name: string, klass: CustomElementConstructor, extendsType: string): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function getOffset(el) {
|
|
2
|
+
const rect = el.getBoundingClientRect();
|
|
3
|
+
return {
|
|
4
|
+
top: rect.top,
|
|
5
|
+
left: rect.left,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function matches(el, selector) {
|
|
9
|
+
const matchesFn = el.matches ||
|
|
10
|
+
el.matchesSelector ||
|
|
11
|
+
el.msMatchesSelector ||
|
|
12
|
+
el.mozMatchesSelector ||
|
|
13
|
+
el.webkitMatchesSelector ||
|
|
14
|
+
el.oMatchesSelector;
|
|
15
|
+
return matchesFn.call(el, selector);
|
|
16
|
+
}
|
|
17
|
+
export function isElementHidden(elem) {
|
|
18
|
+
const display = (elem.style && elem.style.display) || getComputedStyle(elem)['display'];
|
|
19
|
+
return ((elem.offsetWidth <= 0 && elem.offsetHeight <= 0) || display === 'none');
|
|
20
|
+
}
|
|
21
|
+
export function isElementVisible(elem) {
|
|
22
|
+
return !isElementHidden(elem);
|
|
23
|
+
}
|
|
24
|
+
export function extend(defaults, options) {
|
|
25
|
+
const obj = {};
|
|
26
|
+
for (const i in defaults) {
|
|
27
|
+
obj[i] = defaults[i];
|
|
28
|
+
}
|
|
29
|
+
for (const i in options) {
|
|
30
|
+
if (obj[i] instanceof Array && options[i] instanceof Array) {
|
|
31
|
+
obj[i] = obj[i].concat(options[i]);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
obj[i] = options[i];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return obj;
|
|
38
|
+
}
|
|
39
|
+
export function getClassStr(classes) {
|
|
40
|
+
return classes.join(' ');
|
|
41
|
+
}
|
|
42
|
+
export function preventDefaultContextTap() {
|
|
43
|
+
const contextMenuAreas = document.getElementsByClassName('cy-context-menus-cxt-menu');
|
|
44
|
+
for (const cxtMenuArea of contextMenuAreas) {
|
|
45
|
+
cxtMenuArea.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function setBooleanAttribute(element, attribute, boolValue) {
|
|
49
|
+
if (boolValue) {
|
|
50
|
+
element.setAttribute(attribute, '');
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
element.removeAttribute(attribute);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function isIn({ x, y }, element) {
|
|
57
|
+
const rect = element.getBoundingClientRect();
|
|
58
|
+
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
59
|
+
}
|
|
60
|
+
export function getDimensionsHidden(element) {
|
|
61
|
+
element.style.opacity = '0';
|
|
62
|
+
element.style.display = 'block';
|
|
63
|
+
const rect = element.getBoundingClientRect();
|
|
64
|
+
element.style.opacity = '1';
|
|
65
|
+
element.style.display = 'none';
|
|
66
|
+
return rect;
|
|
67
|
+
}
|
|
68
|
+
export function defineCustomElement(name, klass, extendsType) {
|
|
69
|
+
if (typeof customElements.get(name) === 'undefined') {
|
|
70
|
+
customElements.define(name, klass, { extends: extendsType });
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -84,3 +84,6 @@ export { KeyboardCustomLayouts } from './VirtualKeyboard/KeyboardCustomLayout';
|
|
|
84
84
|
export type { KeyboardLayout } from './VirtualKeyboard/KeyboardCustomLayout';
|
|
85
85
|
export { default as AppCopyText } from './AppCopyText';
|
|
86
86
|
export { default as AppConsent } from './AppConsent';
|
|
87
|
+
export { default as CytoscapeMenuCanvas } from './CytoscapeMenu/CytoscapeMenuCanvas';
|
|
88
|
+
export { registerCytoscapeContextMenus, contextMenus, } from './CytoscapeMenu';
|
|
89
|
+
export type { CytoscapeContextMenuOptions, MenuItemOption, ContextMenusInstance, } from './CytoscapeMenu';
|
package/dist/index.js
CHANGED
|
@@ -64,3 +64,5 @@ export { default as VirtualKeyboard } from './VirtualKeyboard';
|
|
|
64
64
|
export { KeyboardCustomLayouts } from './VirtualKeyboard/KeyboardCustomLayout';
|
|
65
65
|
export { default as AppCopyText } from './AppCopyText';
|
|
66
66
|
export { default as AppConsent } from './AppConsent';
|
|
67
|
+
export { default as CytoscapeMenuCanvas } from './CytoscapeMenu/CytoscapeMenuCanvas';
|
|
68
|
+
export { registerCytoscapeContextMenus, contextMenus, } from './CytoscapeMenu';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tycho-components",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
11
|
"require": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./cytoscape-context-menus": {
|
|
14
|
+
"types": "./dist/cytoscape-context-menus.d.ts",
|
|
15
|
+
"import": "./dist/cytoscape-context-menus.js",
|
|
16
|
+
"require": "./dist/cytoscape-context-menus.js"
|
|
17
|
+
},
|
|
13
18
|
"./dist/styles/main": "./dist/styles/main.scss"
|
|
14
19
|
},
|
|
15
20
|
"files": [
|