From 54b7210b9160c4aa3fffa9e29737111593af6512 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 5 Aug 2021 18:58:49 +0200 Subject: feature: context menu and slightly less jumpy graph --- pages/index.tsx | 165 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 139 insertions(+), 26 deletions(-) (limited to 'pages') diff --git a/pages/index.tsx b/pages/index.tsx index e298695..fb12906 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -21,7 +21,7 @@ import { GraphData, NodeObject, LinkObject } from 'force-graph' import { useWindowSize } from '@react-hook/window-size' import { useAnimation } from '@lilib/hooks' -import { Box, useTheme } from '@chakra-ui/react' +import { Box, useDisclosure, useTheme } from '@chakra-ui/react' import { initialPhysics, @@ -33,11 +33,13 @@ import { TagColors, } from '../components/config' import { Tweaks } from '../components/tweaks' +import { ContextMenu } from '../components/contextmenu' import { ThemeContext, ThemeContextProps } from '../util/themecontext' import SpriteText from 'three-spritetext' import ReconnectingWebSocket from 'reconnecting-websocket' +import { sendJson } from 'next/dist/next-server/server/api-utils' // react-force-graph fails on import when server-rendered // https://github.com/vasturiano/react-force-graph/issues/155 @@ -92,8 +94,14 @@ export function GraphPage() { const nodeByIdRef = useRef({}) const linksByNodeIdRef = useRef({}) const tagsRef = useRef([]) + const graphRef = useRef(null) + + const currentGraphDataRef = useRef({ nodes: [], links: [] }) const updateGraphData = (orgRoamGraphData: OrgRoamGraphReponse) => { + const currentGraphData = currentGraphDataRef.current + const oldNodeById = nodeByIdRef.current + const oldLinksByNodeId = linksByNodeIdRef.current tagsRef.current = orgRoamGraphData.tags ?? [] const nodesByFile = orgRoamGraphData.nodes.reduce((acc, node) => { return { @@ -141,11 +149,63 @@ export function GraphPage() { links, } - // react-force-graph modifies the graph data implicitly, - // so we make sure there's no overlap between the objects we pass it and - // nodeByIdRef, linksByNodeIdRef - const orgRoamGraphDataClone = JSON.parse(JSON.stringify(orgRoamGraphDataWithFileLinks)) - setGraphData(orgRoamGraphDataClone) + if (!currentGraphData.nodes.length) { + // react-force-graph modifies the graph data implicitly, + // so we make sure there's no overlap between the objects we pass it and + // nodeByIdRef, linksByNodeIdRef + const orgRoamGraphDataClone = JSON.parse(JSON.stringify(orgRoamGraphDataWithFileLinks)) + currentGraphDataRef.current = orgRoamGraphDataClone + setGraphData(orgRoamGraphDataClone) + return + } + + const newNodes = [ + ...currentGraphData.nodes.map((node: NodeObject) => { + const newNode = nodeByIdRef.current[node.id!] ?? false + if (!newNode) { + return + } + return { ...node, ...newNode } + }), + ...Object.keys(nodeByIdRef.current) + .filter((id) => !oldNodeById[id]) + .map((id) => nodeByIdRef.current[id] as NodeObject), + ] + + /* const currentGraphIndexByLink = currentGraphData.links.reduce<{[key: string]: number}>((acc, link, index) => { +* const [source, target] = normalizeLinkEnds(link) +* const sourceTarget=source+target +* return { +* ...acc, +* [sourceTarget]: index +* } +},{}) */ + + /* const newLinks = graphData!.links.filter(link => { + * const [source, target] = normalizeLinkEnds(link) + * if (!nodeByIdRef.current[source] || !nodeByIdRef.current[target]) { + * return false + * } + * if (!linksByNodeIdRef.current[source]!.some(link => link.target === target || link.source === target) + * && !linksByNodeIdRef.current[target]!.some(link => link.target === source || link.source === source)) { + * return false + * } + * return true + * }) + * console.log(newLinks) + * console.log(currentGraphData.links) */ + /* ...Object.keys(linksByNodeIdRef.current).flatMap((id) => { +if (!oldLinksByNodeId[id]!) { +return linksByNodeIdRef.current![id!]! +} +return linksByNodeIdRef.current![id]!.filter(link => { +const [source, target] = normalizeLinkEnds(link) +return !oldLinksByNodeId[id]!.some(oldLink => oldLink.source === source && oldLink.target === target)! +}) ?? [] +})] */ + const fg = graphRef.current + fg.cooldownTicks = 0 + setGraphData({ nodes: newNodes as NodeObject[], links: links }) } const { setEmacsTheme } = useContext(ThemeContext) @@ -156,7 +216,6 @@ export function GraphPage() { const scopeRef = useRef({ nodeIds: [] }) const behaviorRef = useRef(initialBehavior) behaviorRef.current = behavior - const graphRef = useRef(null) const WebSocketRef = useRef(null) scopeRef.current = scope @@ -269,7 +328,7 @@ export function GraphPage() { } return ( - + - + (ThemeContext) - const handleClick = (click: string, node: NodeObject) => { + const handleLocal = (node: OrgRoamNode, add: string) => { + if (scope.nodeIds.includes(node.id as string)) { + return + } + if (add === 'replace') { + setScope({ nodeIds: [node.id] }) + return + } + setScope((currentScope: Scope) => ({ + ...currentScope, + nodeIds: [...currentScope.nodeIds, node.id as string], + })) + return + } + + const sendMessageToEmacs = (command: string, data: {}) => { + webSocket.send(JSON.stringify({ command: command, data: data })) + } + const openNodeInEmacs = (node: OrgRoamNode) => { + sendMessageToEmacs('open', { id: node.id }) + } + + const deleteNodeInEmacs = (node: OrgRoamNode) => { + console.log('deletin') + if (node.level !== 0) { + return + } + console.log('deletin') + sendMessageToEmacs('delete', { id: node.id, file: node.file }) + } + + const handleClick = (click: string, node: OrgRoamNode) => { switch (click) { //mouse.highlight: case mouse.local: { - if (scope.nodeIds.includes(node.id as string)) { - break - } - setScope((currentScope: Scope) => ({ - ...currentScope, - nodeIds: [...currentScope.nodeIds, node.id as string], - })) + handleLocal(node, behavior.localSame) break } case mouse.follow: { - webSocket.send(node.id) + openNodeInEmacs(node) break } default: @@ -423,8 +507,8 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { const filteredLinksByNodeId = useRef({}) const filteredGraphData = useMemo(() => { hiddenNodeIdsRef.current = {} - const filteredNodes = graphData.nodes - .filter((nodeArg) => { + const filteredNodes = graphData?.nodes + ?.filter((nodeArg) => { const node = nodeArg as OrgRoamNode if ( filter.tagsBlacklist.length && @@ -538,7 +622,7 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { physics.collision ? d3.forceCollide().radius(physics.collisionStrength) : null, ) })() - }) + }, [physics]) // Normally the graph doesn't update when you just change the physics parameters // This forces the graph to make a small update when you do @@ -731,6 +815,16 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { [visuals.labelBackgroundColor, emacsTheme], ) + const { isOpen, onOpen, onClose } = useDisclosure() + const [rightClickedNode, setRightClickedNode] = useState(null) + const [contextPos, setContextPos] = useState([0, 0]) + const openContextMenu = (node: OrgRoamNode, event: any) => { + setContextPos([event.pageX, event.pageY]) + setRightClickedNode(node) + onOpen() + console.log(event) + } + const graphCommonProps: ComponentPropsWithoutRef = { graphData: scopedGraphData, width: windowWidth, @@ -863,7 +957,9 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { d3AlphaMin: physics.alphaMin, d3VelocityDecay: physics.velocityDecay, - onNodeClick: (node: NodeObject, event: any) => { + onNodeClick: (nodeArg: NodeObject, event: any) => { + const node = nodeArg as OrgRoamNode + onClose() const isDoubleClick = event.timeStamp - lastNodeClickRef.current < 400 lastNodeClickRef.current = event.timeStamp if (isDoubleClick) { @@ -872,6 +968,7 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { return handleClick('click', node) }, onBackgroundClick: () => { + onClose() setHoverNode(null) if (scope.nodeIds.length === 0) { return @@ -892,13 +989,29 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { } setHoverNode(node) }, - onNodeRightClick: (node) => { - handleClick('right', node) + onNodeRightClick: (nodeArg, event) => { + const node = nodeArg as OrgRoamNode + openContextMenu(node, event) + + //handleClick('right', node) }, } return ( -
+ + {isOpen && ( + + )} {threeDim ? ( )} -
+
) }) -- cgit v1.2.3