1+ // file: managerFloatingActionPanel.ts
2+
3+ import cytoscape from 'cytoscape' ;
4+ import { ManagerAddContainerlabNode } from './managerAddContainerlabNode' ;
5+ import { getGroupManager } from '../../common/core/managerRegistry' ;
6+ import { log } from '../../common/logging/webviewLogger' ;
7+
8+ /**
9+ * ManagerFloatingActionPanel handles the floating action button (FAB) and radial menu
10+ * for quickly adding nodes, groups, and other elements to the topology.
11+ */
12+ export class ManagerFloatingActionPanel {
13+ private cy : cytoscape . Core ;
14+ private addNodeManager : ManagerAddContainerlabNode ;
15+ private isMenuOpen : boolean = false ;
16+ private fabMain : HTMLElement | null = null ;
17+ private radialMenu : HTMLElement | null = null ;
18+
19+ constructor ( cy : cytoscape . Core , addNodeManager : ManagerAddContainerlabNode ) {
20+ this . cy = cy ;
21+ this . addNodeManager = addNodeManager ;
22+ this . initializePanel ( ) ;
23+ }
24+
25+ /**
26+ * Initializes the floating action panel and sets up event listeners
27+ */
28+ private initializePanel ( ) : void {
29+ // Get DOM elements
30+ this . fabMain = document . getElementById ( 'fab-main' ) ;
31+ this . radialMenu = document . getElementById ( 'radial-menu' ) ;
32+
33+ if ( ! this . fabMain || ! this . radialMenu ) {
34+ log . error ( 'Floating action panel elements not found' ) ;
35+ return ;
36+ }
37+
38+ // Main FAB click handler
39+ this . fabMain . addEventListener ( 'click' , ( e ) => {
40+ e . stopPropagation ( ) ;
41+ this . toggleMenu ( ) ;
42+ } ) ;
43+
44+ // Radial menu item handlers
45+ const addNodeBtn = document . getElementById ( 'radial-add-node' ) ;
46+ const addGroupBtn = document . getElementById ( 'radial-add-group' ) ;
47+ const addTextBtn = document . getElementById ( 'radial-add-text' ) ;
48+
49+ if ( addNodeBtn ) {
50+ addNodeBtn . addEventListener ( 'click' , ( e ) => {
51+ e . stopPropagation ( ) ;
52+ this . handleAddNode ( ) ;
53+ } ) ;
54+ }
55+
56+ if ( addGroupBtn ) {
57+ addGroupBtn . addEventListener ( 'click' , ( e ) => {
58+ e . stopPropagation ( ) ;
59+ this . handleAddGroup ( ) ;
60+ } ) ;
61+ }
62+
63+ if ( addTextBtn ) {
64+ // Future feature - text annotations
65+ addTextBtn . addEventListener ( 'click' , ( e ) => {
66+ e . stopPropagation ( ) ;
67+ log . info ( 'Text annotations coming soon' ) ;
68+ } ) ;
69+ }
70+
71+ // Close menu when clicking outside
72+ document . addEventListener ( 'click' , ( e ) => {
73+ if ( this . isMenuOpen && ! this . fabMain ?. contains ( e . target as Node ) &&
74+ ! this . radialMenu ?. contains ( e . target as Node ) ) {
75+ this . closeMenu ( ) ;
76+ }
77+ } ) ;
78+
79+ // Close menu on Escape key
80+ document . addEventListener ( 'keydown' , ( e ) => {
81+ if ( e . key === 'Escape' && this . isMenuOpen ) {
82+ this . closeMenu ( ) ;
83+ }
84+ } ) ;
85+ }
86+
87+ /**
88+ * Toggles the radial menu open/closed state
89+ */
90+ private toggleMenu ( ) : void {
91+ if ( this . isMenuOpen ) {
92+ this . closeMenu ( ) ;
93+ } else {
94+ this . openMenu ( ) ;
95+ }
96+ }
97+
98+ /**
99+ * Opens the radial menu
100+ */
101+ private openMenu ( ) : void {
102+ if ( ! this . fabMain || ! this . radialMenu ) return ;
103+
104+ this . isMenuOpen = true ;
105+ this . fabMain . classList . add ( 'active' ) ;
106+ this . radialMenu . classList . add ( 'active' ) ;
107+
108+ // Rotate the plus icon to X
109+ const icon = this . fabMain . querySelector ( 'i' ) ;
110+ if ( icon ) {
111+ icon . style . transform = 'rotate(45deg)' ;
112+ }
113+
114+ log . debug ( 'Floating action menu opened' ) ;
115+ }
116+
117+ /**
118+ * Closes the radial menu
119+ */
120+ private closeMenu ( ) : void {
121+ if ( ! this . fabMain || ! this . radialMenu ) return ;
122+
123+ this . isMenuOpen = false ;
124+ this . fabMain . classList . remove ( 'active' ) ;
125+ this . radialMenu . classList . remove ( 'active' ) ;
126+
127+ // Rotate icon back to plus
128+ const icon = this . fabMain . querySelector ( 'i' ) ;
129+ if ( icon ) {
130+ icon . style . transform = 'rotate(0deg)' ;
131+ }
132+
133+ log . debug ( 'Floating action menu closed' ) ;
134+ }
135+
136+ /**
137+ * Handles adding a new node to the topology
138+ */
139+ private handleAddNode ( ) : void {
140+ log . debug ( 'Adding new node via floating action panel' ) ;
141+
142+ // Get viewport center for positioning
143+ const extent = this . cy . extent ( ) ;
144+ const viewportCenterX = ( extent . x1 + extent . x2 ) / 2 ;
145+ const viewportCenterY = ( extent . y1 + extent . y2 ) / 2 ;
146+
147+ // Create a synthetic event object for the add node manager
148+ const syntheticEvent : cytoscape . EventObject = {
149+ type : 'click' ,
150+ target : this . cy ,
151+ cy : this . cy ,
152+ namespace : '' ,
153+ timeStamp : Date . now ( ) ,
154+ position : {
155+ x : viewportCenterX ,
156+ y : viewportCenterY
157+ } ,
158+ renderedPosition : {
159+ x : viewportCenterX ,
160+ y : viewportCenterY
161+ } ,
162+ originalEvent : new MouseEvent ( 'click' )
163+ } as cytoscape . EventObject ;
164+
165+ // Add the node
166+ this . addNodeManager . viewportButtonsAddContainerlabNode ( this . cy , syntheticEvent ) ;
167+
168+ // Close the menu after adding
169+ this . closeMenu ( ) ;
170+
171+ // Optionally, open the node editor for the newly added node
172+ const newNode = this . cy . nodes ( ) . last ( ) ;
173+ // Access viewportPanels through topoViewerState
174+ const state = ( window as any ) . topoViewerState ;
175+ if ( newNode && state ?. editorEngine ?. viewportPanels ) {
176+ setTimeout ( ( ) => {
177+ state . editorEngine . viewportPanels . panelNodeEditor ( newNode ) ;
178+ } , 100 ) ;
179+ }
180+ }
181+
182+ /**
183+ * Handles adding a new group to the topology
184+ */
185+ private handleAddGroup ( ) : void {
186+ log . debug ( 'Adding new group via floating action panel' ) ;
187+
188+ const groupManager = getGroupManager ( this . cy , 'edit' ) ;
189+ if ( ! groupManager ) {
190+ log . error ( 'Group manager not available' ) ;
191+ return ;
192+ }
193+
194+ // Use the same method as the navbar button
195+ groupManager . viewportButtonsAddGroup ( ) ;
196+
197+ // Close the menu after adding
198+ this . closeMenu ( ) ;
199+
200+ log . info ( 'Added new group via floating action panel' ) ;
201+ }
202+
203+ /**
204+ * Shows or hides the floating action panel
205+ */
206+ public setVisibility ( visible : boolean ) : void {
207+ const panel = document . getElementById ( 'floating-action-panel' ) ;
208+ if ( panel ) {
209+ panel . style . display = visible ? 'block' : 'none' ;
210+ }
211+ }
212+
213+ /**
214+ * Updates the panel position if needed (e.g., to avoid overlapping with other UI elements)
215+ */
216+ public updatePosition ( bottom : number = 4 , left : number = 4 ) : void {
217+ const panel = document . getElementById ( 'floating-action-panel' ) ;
218+ if ( panel ) {
219+ panel . style . bottom = `${ bottom } rem` ;
220+ panel . style . left = `${ left } rem` ;
221+ }
222+ }
223+ }
0 commit comments