11import type { DocifyViewModel } from '../view-model/docify.view-model'
2+ import type { VsCodeApi } from '../../webview/panel.view-model'
23import MermaidRenderer from '../../webview/mermaid-renderer'
34import { DiagramControls } from '../../webview/diagram-controls'
45
56const DOM_SETTLE_DELAY_MS = 150
7+ const MIN_CLICKABLE_STROKE_WIDTH = 8
8+ const HOVER_STROKE_WIDTH = 12
69
710/**
811 * DocifyTabView - Manages the DOM for the docify tab in the webview
@@ -13,10 +16,12 @@ export class DocifyTabView {
1316 private container : HTMLElement
1417 private markdownRenderer = new MermaidRenderer ( )
1518 private diagramControls : Map < string , DiagramControls > = new Map ( )
19+ private vscode : VsCodeApi
1620
17- constructor ( viewModel : DocifyViewModel , container : HTMLElement ) {
21+ constructor ( viewModel : DocifyViewModel , container : HTMLElement , vscode : VsCodeApi ) {
1822 this . viewModel = viewModel
1923 this . container = container
24+ this . vscode = vscode
2025 this . bindViewModel ( )
2126 this . initialize ( )
2227 }
@@ -40,7 +45,7 @@ export class DocifyTabView {
4045 * Initialize with default state
4146 */
4247 public initialize ( ) : void {
43- ; ( this . container as any ) . innerHTML = '<em>Initializing...</em>'
48+ ( this . container as any ) . innerHTML = '<em>Initializing...</em>'
4449 }
4550
4651 /**
@@ -53,11 +58,11 @@ export class DocifyTabView {
5358 this . cleanupDiagramControls ( )
5459
5560 if ( format === 'html' ) {
56- ; ( this . container as any ) . innerHTML = content
61+ ( this . container as any ) . innerHTML = content
5762 } else {
5863 // For markdown, render it through MermaidRenderer with source file path for image resolution
59- const renderedHtml = await this . markdownRenderer . render ( content , sourceFile )
60- ; ( this . container as any ) . innerHTML = renderedHtml
64+ const renderedHtml = await this . markdownRenderer . render ( content , sourceFile ) ;
65+ ( this . container as any ) . innerHTML = renderedHtml
6166
6267 // Initialize pan/zoom on all rendered diagrams
6368 this . initializePanZoomForDiagrams ( )
@@ -90,10 +95,161 @@ export class DocifyTabView {
9095 controls . createControls ( container as HTMLElement )
9196 this . diagramControls . set ( diagramId , controls )
9297 }
98+
99+ // Add click event listeners to Mermaid diagram nodes
100+ this . addClickHandlersToMermaidDiagram ( container as HTMLElement )
93101 } )
94102 } , DOM_SETTLE_DELAY_MS )
95103 }
96104
105+ /**
106+ * Add click event listeners to Mermaid diagram nodes to enable selection
107+ */
108+ private addClickHandlersToMermaidDiagram ( container : HTMLElement ) : void {
109+ const svg = container . querySelector ( 'svg' )
110+ if ( ! svg ) {
111+ console . warn ( '[docify-tab] No SVG found in container' )
112+ return
113+ }
114+
115+ console . log ( '[docify-tab] Setting up click handlers for Mermaid diagram' )
116+ const nodeGroups = svg . querySelectorAll ( 'g.node' )
117+ console . log ( `[docify-tab] Found ${ nodeGroups . length } node groups in diagram` )
118+
119+ nodeGroups . forEach ( nodeGroup => {
120+ // Extract the node ID from the group's ID attribute
121+ // Mermaid generates IDs like "flowchart-conference-website-123" for node "conference-website"
122+ const fullId = nodeGroup . getAttribute ( 'id' )
123+ if ( ! fullId ) return
124+
125+ // Extract the actual node ID by removing the diagram prefix and suffix
126+ const nodeId = this . extractNodeIdFromMermaidElement ( fullId )
127+ if ( ! nodeId ) return
128+
129+ console . log ( `[docify-tab] Processing node: ${ fullId } -> ${ nodeId } ` ) ;
130+
131+ // Make the entire node group clickable (includes shape + label)
132+ ( nodeGroup as SVGElement ) . style . cursor = 'pointer' ;
133+ ( nodeGroup as SVGElement ) . style . pointerEvents = 'all'
134+
135+ // Prevent text selection cursor on labels
136+ const labels = nodeGroup . querySelectorAll ( 'text, tspan, foreignObject' )
137+ labels . forEach ( label => {
138+ ( label as SVGElement ) . style . cursor = 'pointer' ;
139+ ( label as SVGElement ) . style . userSelect = 'none' ;
140+ ( label as SVGElement ) . style . pointerEvents = 'none' // Let clicks bubble to parent group
141+ } )
142+
143+ // Add click event listener to the entire node group
144+ nodeGroup . addEventListener ( 'click' , ( event ) => {
145+ event . stopPropagation ( )
146+ event . preventDefault ( )
147+ console . log ( `[docify-tab] Clicked on node: ${ nodeId } ` )
148+ // Send selection message to the extension
149+ this . vscode . postMessage ( { type : 'selected' , id : nodeId } )
150+ } )
151+ } )
152+
153+ // Also handle edge clicks (relationships)
154+ const edgePaths = svg . querySelectorAll ( 'g.edgePath' )
155+ console . log ( `[docify-tab] Found ${ edgePaths . length } edge paths in diagram` )
156+
157+ edgePaths . forEach ( edgePath => {
158+ const fullId = edgePath . getAttribute ( 'id' )
159+ if ( ! fullId ) return
160+
161+ // Edge IDs are typically formatted differently, extract relationship ID
162+ const relationshipId = this . extractRelationshipIdFromMermaidElement ( fullId )
163+ if ( ! relationshipId ) return
164+
165+ console . log ( `[docify-tab] Processing edge: ${ fullId } -> ${ relationshipId } ` ) ;
166+
167+ // Find the path element within the edge group
168+ const path = edgePath . querySelector ( 'path.path' )
169+ if ( ! path ) {
170+ console . warn ( `[docify-tab] No path found for edge ${ relationshipId } ` )
171+ return
172+ }
173+
174+ // Make the path clickable - increase stroke width for easier clicking
175+ path . classList . add ( 'clickable-edge' ) ;
176+ ( path as SVGElement ) . style . cursor = 'pointer' ;
177+ ( path as SVGElement ) . style . pointerEvents = 'visibleStroke' // Make the visible stroke area clickable
178+
179+ // Store original stroke width and increase for clickability
180+ const originalStrokeWidth = window . getComputedStyle ( path as Element ) . strokeWidth ;
181+ path . setAttribute ( 'data-original-stroke-width' , originalStrokeWidth )
182+
183+ // Increase stroke width for better clickability
184+ const currentWidth = parseFloat ( originalStrokeWidth ) || 2 ;
185+ ( path as SVGElement ) . style . strokeWidth = `${ Math . max ( currentWidth , MIN_CLICKABLE_STROKE_WIDTH ) } px`
186+
187+ // Add hover effect via event listeners instead of CSS (more reliable for SVG)
188+ path . addEventListener ( 'mouseenter' , ( ) => {
189+ ( path as SVGElement ) . style . strokeWidth = `${ HOVER_STROKE_WIDTH } px`
190+ } )
191+ path . addEventListener ( 'mouseleave' , ( ) => {
192+ const baseWidth = parseFloat ( path . getAttribute ( 'data-original-stroke-width' ) || '2' ) ;
193+ ( path as SVGElement ) . style . strokeWidth = `${ Math . max ( baseWidth , MIN_CLICKABLE_STROKE_WIDTH ) } px`
194+ } )
195+
196+ // Add click event listener to the path
197+ path . addEventListener ( 'click' , ( event ) => {
198+ event . stopPropagation ( )
199+ event . preventDefault ( )
200+ console . log ( `[docify-tab] Clicked on relationship: ${ relationshipId } ` )
201+ this . vscode . postMessage ( { type : 'selected' , id : relationshipId } )
202+ } )
203+ } )
204+
205+ console . log ( '[docify-tab] Click handlers attached successfully' )
206+ }
207+
208+ /**
209+ * Extract CALM node ID from Mermaid-generated element ID.
210+ *
211+ * Expected input format: Mermaid typically generates IDs like "flowchart-conference-website-123".
212+ * This function removes the "flowchart-" prefix and the trailing numeric suffix.
213+ *
214+ * Example:
215+ * Input: "flowchart-conference-website-123"
216+ * Output: "conference-website"
217+ *
218+ * @param mermaidId The Mermaid-generated element ID string.
219+ * @returns The extracted node ID, or null if extraction fails.
220+ */
221+ private extractNodeIdFromMermaidElement ( mermaidId : string ) : string | null {
222+ // Remove common Mermaid prefixes
223+ let cleaned = mermaidId . replace ( / ^ f l o w c h a r t - / , '' )
224+
225+ // Remove trailing numbers (Mermaid appends random numbers)
226+ // Match everything except the last segment if it's purely numeric
227+ const match = cleaned . match ( / ^ ( .+ ?) - \d + $ / )
228+ if ( match ) {
229+ return match [ 1 ]
230+ }
231+
232+ // If no numeric suffix, return the cleaned ID
233+ return cleaned || null
234+ }
235+
236+ /**
237+ * Extract CALM relationship ID from Mermaid-generated edge element ID
238+ * Mermaid edge IDs are formatted like "L-node1-node2-0" or similar
239+ */
240+ private extractRelationshipIdFromMermaidElement ( mermaidId : string ) : string | null {
241+ // Mermaid edge IDs often start with "L-" or "LE-"
242+ let cleaned = mermaidId . replace ( / ^ L [ E ] ? - / , '' )
243+
244+ // Remove trailing numbers
245+ const match = cleaned . match ( / ^ ( .+ ?) - \d + $ / )
246+ if ( match ) {
247+ return match [ 1 ]
248+ }
249+
250+ return cleaned || null
251+ }
252+
97253 /**
98254 * Clean up diagram controls
99255 */
@@ -107,7 +263,7 @@ export class DocifyTabView {
107263 * Render docify error
108264 */
109265 private renderError ( error : string ) : void {
110- ; ( this . container as any ) . innerHTML = `<div style="color:var(--vscode-editorError-foreground)">Error: ${ this . escapeHtml ( error ) } </div>`
266+ ( this . container as any ) . innerHTML = `<div style="color:var(--vscode-editorError-foreground)">Error: ${ this . escapeHtml ( error ) } </div>`
111267 }
112268
113269 /**
@@ -134,7 +290,7 @@ export class DocifyTabView {
134290 * Cleanup event listeners
135291 */
136292 public dispose ( ) : void {
137- this . cleanupDiagramControls ( )
138- ; ( this . container as any ) . innerHTML = ''
293+ this . cleanupDiagramControls ( ) ;
294+ ( this . container as any ) . innerHTML = ''
139295 }
140296}
0 commit comments