Skip to content

Commit a7805ef

Browse files
committed
Enhance search to highlight target ports of attached Literal nodes
1 parent 36717fe commit a7805ef

File tree

2 files changed

+117
-52
lines changed

2 files changed

+117
-52
lines changed

src/components/XircuitsBodyWidget.tsx

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -289,52 +289,65 @@ export const BodyWidget: FC<BodyWidgetProps> = ({
289289
// Execute search command
290290
const searchInputRef = useRef<HTMLInputElement>(null);
291291

292+
const clearPortHover = () => {
293+
document.querySelectorAll('div.port .hover, g.hover')
294+
.forEach(el => el.classList.remove('hover'));
295+
};
296+
297+
const addPortHover = (nodeId: string, portName: string) => {
298+
const selector = `div.port[data-nodeid="${nodeId}"][data-name='${portName}']>div>div`;
299+
document.querySelector(selector)?.classList.add('hover');
300+
};
301+
292302
const executeSearch = useCallback((text: string) => {
293303
const engine = xircuitsApp.getDiagramEngine();
294304
const model = engine.getModel();
295305
const nodes = model.getNodes();
296-
306+
297307
// Deselect all
298308
nodes.forEach(node => {
299309
node.setSelected(false);
300310
node.getOptions().extras.isMatch = false;
301311
node.getOptions().extras.isSelectedMatch = false;
302312
});
303-
313+
clearPortHover();
314+
304315
const query = text.trim();
305316
if (!query) {
306-
setMatchCount(0);
307-
setCurrentMatch(0);
308-
setMatchedIndices([]);
309-
setCurrentMatchIndex(-1);
310-
engine.repaintCanvas();
311-
return;
317+
setMatchCount(0);
318+
setCurrentMatch(0);
319+
setMatchedIndices([]);
320+
setCurrentMatchIndex(-1);
321+
engine.repaintCanvas();
322+
return;
312323
}
313-
324+
314325
const result: SearchResult = searchModel(model, query);
326+
result.portHits?.forEach(({ nodeId, portName }) => addPortHover(nodeId, portName));
327+
315328
setMatchCount(result.count);
316329
setMatchedIndices(result.indices);
317-
330+
318331
if (result.indices.length > 0) {
319332
result.indices.forEach((index, i) => {
320333
const matchNode = nodes[index];
321334
matchNode.getOptions().extras.isMatch = true;
322335
matchNode.getOptions().extras.isSelectedMatch = i === 0;
323336
});
324-
337+
325338
const first = nodes[result.indices[0]];
326339
first.setSelected(true);
327340
centerNodeInView(engine, first);
328341
engine.repaintCanvas();
329342
setCurrentMatch(1);
330343
setCurrentMatchIndex(0);
331344
} else {
332-
setCurrentMatch(0);
333-
setCurrentMatchIndex(-1);
345+
setCurrentMatch(0);
346+
setCurrentMatchIndex(-1);
334347
}
335-
348+
336349
searchInputRef.current?.focus();
337-
}, [xircuitsApp]);
350+
}, [xircuitsApp]);
338351

339352
const navigateMatch = (direction: 'next' | 'prev') => {
340353
const engine = xircuitsApp.getDiagramEngine();
@@ -389,18 +402,19 @@ export const BodyWidget: FC<BodyWidgetProps> = ({
389402
useEffect(() => {
390403
isHoveringControlsRef.current = isHoveringControls;
391404
}, [isHoveringControls]);
392-
405+
393406
useEffect(() => {
394-
if (!showSearch) {
395-
const engine = xircuitsApp.getDiagramEngine();
396-
const nodes = engine.getModel().getNodes();
397-
nodes.forEach(node => {
398-
node.getOptions().extras.isMatch = false;
399-
node.getOptions().extras.isSelectedMatch = false;
400-
});
401-
engine.repaintCanvas();
402-
}
403-
}, [showSearch]);
407+
if (!showSearch) {
408+
clearPortHover();
409+
const engine = xircuitsApp.getDiagramEngine();
410+
const nodes = engine.getModel().getNodes();
411+
nodes.forEach(node => {
412+
node.getOptions().extras.isMatch = false;
413+
node.getOptions().extras.isSelectedMatch = false;
414+
});
415+
engine.repaintCanvas();
416+
}
417+
}, [showSearch]);
404418

405419
const handleMouseMoveCanvas = useCallback(() => {
406420
setShowZoom(true);

src/helpers/search.tsx

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,62 @@
11
import { DiagramModel, NodeModel } from '@projectstorm/react-diagrams';
22

33
export interface SearchResult {
4-
/** total number of matches */
4+
/** total number of matches */
55
count: number;
6-
/** zero-based indices of matching nodes in model.getNodes() */
6+
/** zero-based indices of matching nodes in model.getNodes() */
77
indices: number[];
8+
/** target ports to highlight in UI when a Literal is attached */
9+
portHits: { nodeId: string; portName: string }[];
810
}
911

12+
const getOptions = (n: any) => (n?.getOptions?.() ?? {}) as any;
13+
const getNodeID = (n: any) => n?.getID?.() ?? n?.options?.id ?? getOptions(n)?.id;
14+
const getPort = (n: any) => (Object.values(n?.getPorts?.() ?? {}) as any[])[0];
15+
1016
/**
1117
* Collect searchable texts only from ports (for literals).
1218
*/
1319
function collectLiteralValues(node: NodeModel): string[] {
14-
const texts: string[] = [];
15-
const opts = node.getOptions() as any;
16-
17-
const nodeName = (opts.name || "").toLowerCase();
18-
const isLiteral = nodeName.startsWith("literal");
19-
20-
if (isLiteral) {
21-
Object.values(node.getPorts()).forEach((p: any) => {
22-
const pOpt = p.getOptions?.() ?? {};
23-
if (pOpt.label) {
24-
texts.push(pOpt.label);
25-
}
26-
});
20+
const rawName = String(getOptions(node).name || '');
21+
if (!rawName.startsWith('Literal ')) return [];
22+
23+
const port = getPort(node);
24+
if (!port) return [];
25+
26+
const label = port.getOptions?.()?.label;
27+
return label != null ? [String(label).toLowerCase()] : [];
28+
}
29+
30+
/**
31+
* Return (nodeId, portName) of the opposite end connected to a Literal node.
32+
*/
33+
function getAttachedTargetByPortName(node: NodeModel): { nodeId: string; portName: string }[] {
34+
const results: { nodeId: string; portName: string }[] = [];
35+
36+
const port = getPort(node);
37+
if (!port) return results;
38+
39+
const links = Object.values(port.getLinks?.() ?? {}) as any[];
40+
for (const link of links) {
41+
const src = link.getSourcePort?.();
42+
const trg = link.getTargetPort?.();
43+
44+
// Identify the opposite port on the link
45+
const otherPort = src?.getNode?.() === node ? trg : src;
46+
if (!otherPort) continue;
47+
48+
const otherNodeId = getNodeID(otherPort.getNode?.());
49+
const otherPortName =
50+
otherPort.getName?.() ??
51+
otherPort.options?.name ??
52+
otherPort.getOptions?.()?.name;
53+
54+
if (otherNodeId && otherPortName) {
55+
results.push({ nodeId: String(otherNodeId), portName: String(otherPortName) });
56+
}
2757
}
2858

29-
return texts.filter(Boolean).map((s) => String(s).toLowerCase());
59+
return results;
3060
}
3161

3262
/**
@@ -37,22 +67,43 @@ export function searchModel(model: DiagramModel, text: string): SearchResult {
3767
const nodes = model.getNodes();
3868
const query = text.trim().toLowerCase();
3969
if (!query) {
40-
return { count: 0, indices: [] };
70+
return { count: 0, indices: [], portHits: [] };
4171
}
4272

43-
const indices: number[] = [];
73+
const idToIdx = new Map<string, number>();
74+
nodes.forEach((node: NodeModel, i) => {
75+
const id = getNodeID(node);
76+
if (id) idToIdx.set(String(id), i);
77+
});
78+
79+
const indices = new Set<number>();
80+
const portHits: { nodeId: string; portName: string }[] = [];
81+
82+
nodes.forEach((node, idx) => {
83+
const rawName = String(getOptions(node).name || '');
84+
const nameLower = rawName.toLowerCase();
85+
const texts = [nameLower, ...collectLiteralValues(node)];
86+
if (!texts.some((t) => t.includes(query))) return;
87+
88+
const isLiteral = rawName.startsWith('Literal ');
89+
const isAttached = Boolean(getOptions(node).extras?.attached);
4490

45-
nodes.forEach((node: NodeModel, idx: number) => {
46-
const opts = node.getOptions() as any;
47-
const name: string = (opts.name || '').toString().toLowerCase();
48-
const texts = [name, ...collectLiteralValues(node)];
49-
if (texts.some((t) => t.includes(query))) {
50-
indices.push(idx);
91+
if (isLiteral && isAttached) {
92+
const targets = getAttachedTargetByPortName(node);
93+
portHits.push(...targets);
94+
targets.forEach(({ nodeId }) => {
95+
const i = idToIdx.get(nodeId);
96+
if (typeof i === 'number') indices.add(i);
97+
});
98+
} else {
99+
indices.add(idx);
51100
}
52101
});
53102

103+
const idxArr = Array.from(indices);
54104
return {
55-
count: indices.length,
56-
indices
105+
count: idxArr.length,
106+
indices: idxArr,
107+
portHits
57108
};
58109
}

0 commit comments

Comments
 (0)