Skip to content
This repository has been archived by the owner on Jul 15, 2024. It is now read-only.

Commit

Permalink
Make node expandable (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdlyy authored Feb 8, 2024
1 parent b7d5b16 commit ddaa9bf
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 8 deletions.
12 changes: 11 additions & 1 deletion packages/frontend/src/store/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,18 @@ export interface State {
readonly shiftKey: boolean
readonly spaceKey: boolean
}
readonly resizingNode?: {
readonly id: string
readonly initialWidth: number
readonly startX: number
}
readonly mouseUpAction?: DeselectOne | DeselectAllBut
readonly mouseMoveAction?: 'drag' | 'pan' | 'select' | 'select-add'
readonly mouseMoveAction?:
| 'drag'
| 'pan'
| 'select'
| 'select-add'
| 'resize-node'
readonly mouseMove: {
readonly startX: number
readonly startY: number
Expand Down
19 changes: 19 additions & 0 deletions packages/frontend/src/store/actions/onMouseDown.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isResizeHandle } from '../../view/ResizeHandle'
import { State } from '../State'
import { LEFT_MOUSE_BUTTON, MIDDLE_MOUSE_BUTTON } from '../utils/constants'
import { toViewCoordinates } from '../utils/coordinates'
Expand All @@ -8,6 +9,24 @@ export function onMouseDown(
event: MouseEvent,
container: HTMLElement,
): Partial<State> {
// Resize anchor
if (isResizeHandle(event.target)) {
const { nodeId } = event.target.dataset
const node = state.nodes.find((n) => n.simpleNode.id === nodeId)

if (node && nodeId) {
return {
resizingNode: {
id: nodeId,
initialWidth: node.box.width,
startX: event.clientX,
},
mouseMoveAction: 'resize-node',
pressed: { ...state.pressed, leftMouseButton: true },
}
}
}

if (event.button === LEFT_MOUSE_BUTTON && !state.mouseMoveAction) {
if (state.pressed.spaceKey) {
const [x, y] = [event.clientX, event.clientY]
Expand Down
30 changes: 29 additions & 1 deletion packages/frontend/src/store/actions/onMouseMove.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, State } from '../State'
import { LEFT_MOUSE_BUTTON } from '../utils/constants'
import { LEFT_MOUSE_BUTTON, NODE_WIDTH } from '../utils/constants'
import { toViewCoordinates } from '../utils/coordinates'
import { toContainerCoordinates } from '../utils/toContainerCoordinates'
import { updateNodePositions } from '../utils/updateNodePositions'
Expand All @@ -18,6 +18,34 @@ export function onMouseMove(
case undefined: {
return { ...state, mouseUpAction: undefined }
}
case 'resize-node': {
if (!state.resizingNode) {
break
}

const { scale } = state.transform

const dx = event.clientX - state.resizingNode.startX

const newWidth = Math.max(
state.resizingNode.initialWidth + dx / scale,
NODE_WIDTH,
)

const nodes = state.nodes.map((node) =>
node.simpleNode.id === state.resizingNode?.id
? {
...node,
box: { ...node.box, width: newWidth },
}
: node,
)

return updateNodePositions({
...state,
nodes,
})
}
case 'pan': {
const [x, y] = [event.clientX, event.clientY]
return {
Expand Down
19 changes: 15 additions & 4 deletions packages/frontend/src/store/actions/updateNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export function updateNodes(state: State, nodes: SimpleNode[]): Partial<State> {
.filter((node) => oldNodes.has(node.id))
.map((node) => {
const oldNode = oldNodes.get(node.id)
return simpleNodeToNode(node, oldNode?.box.x ?? 0, oldNode?.box.y ?? 0)
return simpleNodeToNode(
node,
oldNode?.box.x ?? 0,
oldNode?.box.y ?? 0,
oldNode?.box.width ?? NODE_WIDTH,
)
})

const addedNodes = nodes
Expand All @@ -37,7 +42,8 @@ export function updateNodes(state: State, nodes: SimpleNode[]): Partial<State> {
const box = getNodeBoxFromStorage(state.projectId, node)
const x = box?.x ?? startX + (NODE_WIDTH + NODE_SPACING) * i
const y = box?.y ?? 0
return simpleNodeToNode(node, x, y)
const width = box?.width ?? NODE_WIDTH
return simpleNodeToNode(node, x, y, width)
})

return updateNodePositions({
Expand Down Expand Up @@ -76,11 +82,16 @@ function getNodeBoxFromStorage(projectId: string, node: SimpleNode) {
return location
}

function simpleNodeToNode(node: SimpleNode, x: number, y: number): Node {
function simpleNodeToNode(
node: SimpleNode,
x: number,
y: number,
width: number,
): Node {
return {
simpleNode: node,
// height will be updated by updateNodePositions
box: { x, y, width: NODE_WIDTH, height: 0 },
box: { x, y, width: width, height: 0 },
fields: node.fields.map((field) => ({
name: field.name,
connection: toConnection(field.connection),
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/store/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const FIELD_HEIGHT = 24
export const NODE_WIDTH = 200
export const NODE_SPACING = 25

export const RESIZE_HANDLE_SPACING = 15

export const ZOOM_SENSITIVITY = 0.02
export const MAX_ZOOM = 3
export const MIN_ZOOM = 0.3
Expand Down
69 changes: 67 additions & 2 deletions packages/frontend/src/view/NodeView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import classNames from 'classnames'
import { useCallback } from 'react'
import { useCallback, useRef } from 'react'

import { Node } from '../store/State'
import { useStore } from '../store/store'
import { NODE_WIDTH, RESIZE_HANDLE_SPACING } from '../store/utils/constants'
import { ResizeHandle } from './ResizeHandle'

export interface NodeViewProps {
node: Node
Expand All @@ -12,17 +15,34 @@ export interface NodeViewProps {
}

export function NodeView(props: NodeViewProps) {
const ref = useRef<HTMLDivElement>(null)

const updateNodeLocations = useStore((state) => state.updateNodeLocations)

const onDiscover = useCallback(() => {
props.onDiscover(props.node.simpleNode.id)
}, [props.onDiscover, props.node.simpleNode.id])

const onDoubleClick = useCallback(() => {
if (!ref.current) {
return
}

const newBox = getLocationByChildWidth(ref.current)

updateNodeLocations({
[props.node.simpleNode.id]: newBox,
})
}, [])

return (
<div
ref={ref}
style={{
left: props.node.box.x,
top: props.node.box.y,
width: props.node.box.width,
height: props.node.box.height,
height: props.node.box.height + RESIZE_HANDLE_SPACING,
}}
className={classNames(
'absolute rounded-md border-2 border-black bg-white',
Expand Down Expand Up @@ -63,6 +83,51 @@ export function NodeView(props: NodeViewProps) {
)}
</div>
))}
<ResizeHandle
nodeId={props.node.simpleNode.id}
onDoubleClick={onDoubleClick}
/>
</div>
)
}

/**
* Render children with out parent constraints to compute actual width we should expand into
*/
function getAbsoluteWidth(element: Element) {
// deep clone to have potential descendants
const clone = element.cloneNode(true) as HTMLElement
clone.style.width = 'auto'
clone.style.position = 'absolute'
clone.style.visibility = 'hidden'
clone.style.pointerEvents = 'none'
clone.style.transform = 'translateZ(0)'

document.body.appendChild(clone)

const { offsetWidth: width } = clone

document.body.removeChild(clone)

return width
}

function getLocationByChildWidth(element: HTMLElement) {
const AUTO_EXPAND_ADDITIONAL_SPACE = 20

const absoluteWidths = Array.from(element.children).map((children) =>
getAbsoluteWidth(children),
)

const newWidth = Math.max(...absoluteWidths, NODE_WIDTH)

const newWidthWithOffset = newWidth + AUTO_EXPAND_ADDITIONAL_SPACE

const newBox = {
x: element.offsetLeft,
y: element.offsetTop,
width: newWidthWithOffset,
}

return newBox
}
36 changes: 36 additions & 0 deletions packages/frontend/src/view/ResizeHandle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MouseEventHandler } from 'react'

const RESIZE_DATA_HANDLE = 'resize'

export function ResizeHandle(props: {
nodeId: string
onDoubleClick: MouseEventHandler<HTMLDivElement>
}) {
const handleDoubleClick: MouseEventHandler<HTMLDivElement> = (event) => {
event.stopPropagation()
props.onDoubleClick(event)
}

return (
<div
className="resize-handle group absolute -right-[2px] -bottom-[2px] flex h-[15px] w-[15px] -rotate-45 cursor-nwse-resize flex-col items-center justify-center gap-0.5 bg-clip-padding"
data-node-id={props.nodeId}
data-node-operation={RESIZE_DATA_HANDLE}
onDoubleClick={handleDoubleClick}
>
<hr className="pointer-events-none w-[23px] border-t-black group-hover:border-t-gray-500" />
<hr className="pointer-events-none w-[17px] border-t-black group-hover:border-t-gray-500" />
<hr className="pointer-events-none w-[11px] border-t-black group-hover:border-t-gray-500" />
</div>
)
}

export function isResizeHandle(
target: EventTarget | null,
): target is HTMLElement {
return Boolean(
target instanceof HTMLElement &&
target.dataset.nodeOperation === RESIZE_DATA_HANDLE &&
target.dataset.nodeId,
)
}

0 comments on commit ddaa9bf

Please sign in to comment.