From 36717fe55b21c17fb6b7b2f2f6e7f4dc234b4588 Mon Sep 17 00:00:00 2001 From: rabea-Al Date: Tue, 30 Sep 2025 22:21:46 +0800 Subject: [PATCH 1/2] Add literal values support in search --- src/helpers/search.tsx | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/helpers/search.tsx b/src/helpers/search.tsx index 39b86f40..395851ed 100644 --- a/src/helpers/search.tsx +++ b/src/helpers/search.tsx @@ -8,7 +8,30 @@ export interface SearchResult { } /** - * Search all nodes’ `options.name` (case-insensitive). + * Collect searchable texts only from ports (for literals). + */ +function collectLiteralValues(node: NodeModel): string[] { + const texts: string[] = []; + const opts = node.getOptions() as any; + + const nodeName = (opts.name || "").toLowerCase(); + const isLiteral = nodeName.startsWith("literal"); + + if (isLiteral) { + Object.values(node.getPorts()).forEach((p: any) => { + const pOpt = p.getOptions?.() ?? {}; + if (pOpt.label) { + texts.push(pOpt.label); + } + }); + } + + return texts.filter(Boolean).map((s) => String(s).toLowerCase()); +} + +/** + * Search all nodes’ `options.name` (case-insensitive), + * and also inside literal values from ports. */ export function searchModel(model: DiagramModel, text: string): SearchResult { const nodes = model.getNodes(); @@ -18,14 +41,13 @@ export function searchModel(model: DiagramModel, text: string): SearchResult { } const indices: number[] = []; - const matches: string[] = []; nodes.forEach((node: NodeModel, idx: number) => { const opts = node.getOptions() as any; - const name: string = (opts.name || '').toString(); - if (name.toLowerCase().includes(query)) { - indices.push(idx); - matches.push(name); + const name: string = (opts.name || '').toString().toLowerCase(); + const texts = [name, ...collectLiteralValues(node)]; + if (texts.some((t) => t.includes(query))) { + indices.push(idx); } }); From a7805efccbccb7c1f08b52472afc45d7a877f41c Mon Sep 17 00:00:00 2001 From: rabea-Al Date: Tue, 7 Oct 2025 00:45:51 +0800 Subject: [PATCH 2/2] Enhance search to highlight target ports of attached Literal nodes --- src/components/XircuitsBodyWidget.tsx | 66 ++++++++++------- src/helpers/search.tsx | 103 +++++++++++++++++++------- 2 files changed, 117 insertions(+), 52 deletions(-) diff --git a/src/components/XircuitsBodyWidget.tsx b/src/components/XircuitsBodyWidget.tsx index c33f7e72..e6d4d6ee 100644 --- a/src/components/XircuitsBodyWidget.tsx +++ b/src/components/XircuitsBodyWidget.tsx @@ -289,39 +289,52 @@ export const BodyWidget: FC = ({ // Execute search command const searchInputRef = useRef(null); + const clearPortHover = () => { + document.querySelectorAll('div.port .hover, g.hover') + .forEach(el => el.classList.remove('hover')); + }; + + const addPortHover = (nodeId: string, portName: string) => { + const selector = `div.port[data-nodeid="${nodeId}"][data-name='${portName}']>div>div`; + document.querySelector(selector)?.classList.add('hover'); + }; + const executeSearch = useCallback((text: string) => { const engine = xircuitsApp.getDiagramEngine(); const model = engine.getModel(); const nodes = model.getNodes(); - + // Deselect all nodes.forEach(node => { node.setSelected(false); node.getOptions().extras.isMatch = false; node.getOptions().extras.isSelectedMatch = false; }); - + clearPortHover(); + const query = text.trim(); if (!query) { - setMatchCount(0); - setCurrentMatch(0); - setMatchedIndices([]); - setCurrentMatchIndex(-1); - engine.repaintCanvas(); - return; + setMatchCount(0); + setCurrentMatch(0); + setMatchedIndices([]); + setCurrentMatchIndex(-1); + engine.repaintCanvas(); + return; } - + const result: SearchResult = searchModel(model, query); + result.portHits?.forEach(({ nodeId, portName }) => addPortHover(nodeId, portName)); + setMatchCount(result.count); setMatchedIndices(result.indices); - + if (result.indices.length > 0) { result.indices.forEach((index, i) => { const matchNode = nodes[index]; matchNode.getOptions().extras.isMatch = true; matchNode.getOptions().extras.isSelectedMatch = i === 0; }); - + const first = nodes[result.indices[0]]; first.setSelected(true); centerNodeInView(engine, first); @@ -329,12 +342,12 @@ export const BodyWidget: FC = ({ setCurrentMatch(1); setCurrentMatchIndex(0); } else { - setCurrentMatch(0); - setCurrentMatchIndex(-1); + setCurrentMatch(0); + setCurrentMatchIndex(-1); } - + searchInputRef.current?.focus(); - }, [xircuitsApp]); + }, [xircuitsApp]); const navigateMatch = (direction: 'next' | 'prev') => { const engine = xircuitsApp.getDiagramEngine(); @@ -389,18 +402,19 @@ export const BodyWidget: FC = ({ useEffect(() => { isHoveringControlsRef.current = isHoveringControls; }, [isHoveringControls]); - + useEffect(() => { - if (!showSearch) { - const engine = xircuitsApp.getDiagramEngine(); - const nodes = engine.getModel().getNodes(); - nodes.forEach(node => { - node.getOptions().extras.isMatch = false; - node.getOptions().extras.isSelectedMatch = false; - }); - engine.repaintCanvas(); -} -}, [showSearch]); + if (!showSearch) { + clearPortHover(); + const engine = xircuitsApp.getDiagramEngine(); + const nodes = engine.getModel().getNodes(); + nodes.forEach(node => { + node.getOptions().extras.isMatch = false; + node.getOptions().extras.isSelectedMatch = false; + }); + engine.repaintCanvas(); + } + }, [showSearch]); const handleMouseMoveCanvas = useCallback(() => { setShowZoom(true); diff --git a/src/helpers/search.tsx b/src/helpers/search.tsx index 395851ed..a849f8f4 100644 --- a/src/helpers/search.tsx +++ b/src/helpers/search.tsx @@ -1,32 +1,62 @@ import { DiagramModel, NodeModel } from '@projectstorm/react-diagrams'; export interface SearchResult { - /** total number of matches */ + /** total number of matches */ count: number; - /** zero-based indices of matching nodes in model.getNodes() */ + /** zero-based indices of matching nodes in model.getNodes() */ indices: number[]; + /** target ports to highlight in UI when a Literal is attached */ + portHits: { nodeId: string; portName: string }[]; } +const getOptions = (n: any) => (n?.getOptions?.() ?? {}) as any; +const getNodeID = (n: any) => n?.getID?.() ?? n?.options?.id ?? getOptions(n)?.id; +const getPort = (n: any) => (Object.values(n?.getPorts?.() ?? {}) as any[])[0]; + /** * Collect searchable texts only from ports (for literals). */ function collectLiteralValues(node: NodeModel): string[] { - const texts: string[] = []; - const opts = node.getOptions() as any; - - const nodeName = (opts.name || "").toLowerCase(); - const isLiteral = nodeName.startsWith("literal"); - - if (isLiteral) { - Object.values(node.getPorts()).forEach((p: any) => { - const pOpt = p.getOptions?.() ?? {}; - if (pOpt.label) { - texts.push(pOpt.label); - } - }); + const rawName = String(getOptions(node).name || ''); + if (!rawName.startsWith('Literal ')) return []; + + const port = getPort(node); + if (!port) return []; + + const label = port.getOptions?.()?.label; + return label != null ? [String(label).toLowerCase()] : []; +} + +/** + * Return (nodeId, portName) of the opposite end connected to a Literal node. + */ +function getAttachedTargetByPortName(node: NodeModel): { nodeId: string; portName: string }[] { + const results: { nodeId: string; portName: string }[] = []; + + const port = getPort(node); + if (!port) return results; + + const links = Object.values(port.getLinks?.() ?? {}) as any[]; + for (const link of links) { + const src = link.getSourcePort?.(); + const trg = link.getTargetPort?.(); + + // Identify the opposite port on the link + const otherPort = src?.getNode?.() === node ? trg : src; + if (!otherPort) continue; + + const otherNodeId = getNodeID(otherPort.getNode?.()); + const otherPortName = + otherPort.getName?.() ?? + otherPort.options?.name ?? + otherPort.getOptions?.()?.name; + + if (otherNodeId && otherPortName) { + results.push({ nodeId: String(otherNodeId), portName: String(otherPortName) }); + } } - return texts.filter(Boolean).map((s) => String(s).toLowerCase()); + return results; } /** @@ -37,22 +67,43 @@ export function searchModel(model: DiagramModel, text: string): SearchResult { const nodes = model.getNodes(); const query = text.trim().toLowerCase(); if (!query) { - return { count: 0, indices: [] }; + return { count: 0, indices: [], portHits: [] }; } - const indices: number[] = []; + const idToIdx = new Map(); + nodes.forEach((node: NodeModel, i) => { + const id = getNodeID(node); + if (id) idToIdx.set(String(id), i); + }); + + const indices = new Set(); + const portHits: { nodeId: string; portName: string }[] = []; + + nodes.forEach((node, idx) => { + const rawName = String(getOptions(node).name || ''); + const nameLower = rawName.toLowerCase(); + const texts = [nameLower, ...collectLiteralValues(node)]; + if (!texts.some((t) => t.includes(query))) return; + + const isLiteral = rawName.startsWith('Literal '); + const isAttached = Boolean(getOptions(node).extras?.attached); - nodes.forEach((node: NodeModel, idx: number) => { - const opts = node.getOptions() as any; - const name: string = (opts.name || '').toString().toLowerCase(); - const texts = [name, ...collectLiteralValues(node)]; - if (texts.some((t) => t.includes(query))) { - indices.push(idx); + if (isLiteral && isAttached) { + const targets = getAttachedTargetByPortName(node); + portHits.push(...targets); + targets.forEach(({ nodeId }) => { + const i = idToIdx.get(nodeId); + if (typeof i === 'number') indices.add(i); + }); + } else { + indices.add(idx); } }); + const idxArr = Array.from(indices); return { - count: indices.length, - indices + count: idxArr.length, + indices: idxArr, + portHits }; }