Skip to content

Commit 4047eeb

Browse files
Enhance sticky note height calculation for note itself and contents (#487)
1 parent 674d14f commit 4047eeb

8 files changed

Lines changed: 70 additions & 44 deletions

File tree

src/main/frontend/app/routes/studio/canvas/flow.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const FlowConfig = {
55
EXIT_DEFAULT_HEIGHT: 100,
66
STICKY_NOTE_DEFAULT_WIDTH: 200,
77
STICKY_NOTE_DEFAULT_HEIGHT: 200,
8+
STICKY_NOTE_MAX_HEIGHT: 500,
89
STICKY_NOTE_BALLOON_WIDTH: 160,
910
STICKY_NOTE_BALLOON_HEIGHT: 58,
1011
COPY_PASTE_OFFSET: 40,

src/main/frontend/app/routes/studio/canvas/flow.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,10 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) {
11471147
data: { content: '' },
11481148
type: 'stickyNote',
11491149
selected: true,
1150+
style: {
1151+
width: FlowConfig.STICKY_NOTE_DEFAULT_WIDTH,
1152+
height: FlowConfig.STICKY_NOTE_DEFAULT_HEIGHT,
1153+
},
11501154
}
11511155

11521156
flowStore.setNodes([...deselectedNodes, stickyNote])

src/main/frontend/app/routes/studio/canvas/nodetypes/sticky-note.tsx

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { type Node, type NodeProps, NodeResizeControl } from '@xyflow/react'
1+
import { type Node, type NodeProps, NodeResizeControl, useUpdateNodeInternals } from '@xyflow/react'
22
import { FlowConfig } from '~/routes/studio/canvas/flow.config'
33
import { ResizeIcon } from '~/routes/studio/canvas/nodetypes/frank-node'
4-
import React, { useEffect, useRef, useState } from 'react'
4+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
55
import useFlowStore from '~/stores/flow-store'
66
import { useNodeContextMenu } from '~/routes/studio/canvas/node-context-menu-context'
77
import useNodeContextStore from '~/stores/node-context-store'
@@ -33,37 +33,42 @@ export default function StickyNoteComponent(properties: NodeProps<StickyNote>) {
3333
const minHeight = FlowConfig.STICKY_NOTE_DEFAULT_HEIGHT
3434
const minWidth = FlowConfig.STICKY_NOTE_DEFAULT_WIDTH
3535
const showNodeContextMenu = useNodeContextMenu()
36-
37-
const [localContent, setLocalContent] = useState(properties.data.content)
36+
const updateNodeInternals = useUpdateNodeInternals()
37+
const containerRef = useRef<HTMLDivElement>(null)
38+
const contentRef = useRef<HTMLDivElement>(null)
3839
const [isOverflowing, setIsOverflowing] = useState(false)
3940

40-
const textareaReference = useRef<HTMLTextAreaElement>(null)
41-
const containerReference = useRef<HTMLDivElement>(null)
42-
4341
const color = properties.data.color ?? 'var(--sticky-color-yellow)'
42+
const content = properties.data.content
43+
44+
const CONTENT_PADDING_Y = 24
45+
46+
useLayoutEffect(() => {
47+
if (properties.data.collapsed || !contentRef.current) return
48+
49+
const naturalHeight = contentRef.current.scrollHeight + CONTENT_PADDING_Y
50+
const clamped = Math.min(
51+
FlowConfig.STICKY_NOTE_MAX_HEIGHT,
52+
Math.max(FlowConfig.STICKY_NOTE_DEFAULT_HEIGHT, naturalHeight),
53+
)
54+
55+
useFlowStore.getState().setStickyHeight(properties.id, clamped)
56+
}, [content, properties.data.collapsed, properties.id])
4457

4558
useEffect(() => {
46-
setLocalContent(properties.data.content)
47-
}, [properties.data.content])
59+
updateNodeInternals(properties.id)
60+
}, [properties.data.collapsed, properties.id, updateNodeInternals])
4861

4962
useEffect(() => {
5063
if (properties.data.collapsed) return
51-
const textarea = textareaReference.current
52-
const container = containerReference.current
53-
if (!textarea || !container) return
54-
55-
const check = () => setIsOverflowing(textarea.scrollHeight > textarea.clientHeight)
64+
const container = containerRef.current
65+
if (!container) return
66+
const check = () => setIsOverflowing(container.scrollHeight > container.clientHeight)
5667
check()
57-
5868
const observer = new ResizeObserver(check)
5969
observer.observe(container)
6070
return () => observer.disconnect()
61-
}, [localContent, properties.data.collapsed])
62-
63-
const updateContent = (changeEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
64-
setLocalContent(changeEvent.target.value)
65-
useFlowStore.getState().setStickyText(properties.id, changeEvent.target.value)
66-
}
71+
}, [content, properties.data.collapsed])
6772

6873
const handleDelete = () => {
6974
useNodeContextStore.getState().setSelectedStickyId(null)
@@ -81,7 +86,9 @@ export default function StickyNoteComponent(properties: NodeProps<StickyNote>) {
8186
className="flex items-center overflow-hidden rounded-lg px-2"
8287
style={{ width: `${FlowConfig.STICKY_NOTE_BALLOON_WIDTH}px`, height: '46px', background: color }}
8388
>
84-
<span className="flex-1 truncate text-xs">{localContent}</span>
89+
<span className="line-clamp-2 flex-1 overflow-hidden text-xs leading-snug whitespace-pre-wrap">
90+
{content}
91+
</span>
8592
<button
8693
className="nodrag ml-1 shrink-0 text-xs hover:cursor-pointer hover:opacity-70"
8794
onClick={(e) => {
@@ -127,7 +134,7 @@ export default function StickyNoteComponent(properties: NodeProps<StickyNote>) {
127134
<ResizeIcon />
128135
</NodeResizeControl>
129136
<div
130-
ref={containerReference}
137+
ref={containerRef}
131138
className={`relative h-full w-full overflow-hidden p-3 text-xs ${properties.selected ? 'ring-1 ring-black/40' : ''}`}
132139
style={{
133140
minHeight: `${minHeight}px`,
@@ -138,19 +145,14 @@ export default function StickyNoteComponent(properties: NodeProps<StickyNote>) {
138145
`,
139146
}}
140147
>
141-
<textarea
142-
ref={textareaReference}
143-
value={localContent}
144-
onChange={updateContent}
145-
className="nodrag h-full w-full resize-none overflow-hidden bg-transparent text-xs leading-snug outline-none"
146-
/>
148+
<div ref={contentRef} className="w-full text-xs leading-snug break-words whitespace-pre-wrap">
149+
{content}
150+
</div>
147151
{isOverflowing && (
148152
<div
149-
className="pointer-events-none absolute right-0 bottom-0 left-0 flex justify-end pr-3 pb-1 text-xs opacity-60"
153+
className="pointer-events-none absolute right-0 bottom-0 left-0 h-8"
150154
style={{ background: `linear-gradient(transparent, ${color})` }}
151-
>
152-
···
153-
</div>
155+
/>
154156
)}
155157
<div className="nodrag absolute top-0 right-5 flex items-center">
156158
<div

src/main/frontend/app/routes/studio/flow-to-xml-parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ function generateFlowElementsXml(nodes: FlowNode[]): string {
326326
}
327327
}
328328

329-
return ` <flow:StickyNote flow:text="${escapeXml(text)}"${colorAttr} flow:x="${roundedX}" flow:y="${roundedY}" flow:width="${width}" flow:height="${height}"${collapsedAttr}${attachedToAttr} />`
329+
return ` <flow:StickyNote${colorAttr} flow:x="${roundedX}" flow:y="${roundedY}" flow:width="${width}" flow:height="${height}"${collapsedAttr}${attachedToAttr}><![CDATA[${text.replaceAll(']]>', ']]]]><![CDATA[>')}]]></flow:StickyNote>`
330330
})
331331

332332
const groupNodesXml = generateGroupNodeXml(groupNodes, groupChildrenMap)

src/main/frontend/app/routes/studio/studio.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ function AttachedNotesPanel({ nodeId }: { nodeId: number }) {
104104
{attachedNotes.map((note) => (
105105
<div
106106
key={note.id}
107-
className="rounded-lg p-2 text-xs leading-snug"
107+
className="rounded-lg p-2 text-xs leading-snug break-words whitespace-pre-wrap"
108108
style={{ background: note.data.color ?? '#fef08a' }}
109109
>
110110
{note.data.content || <span className="opacity-40">Empty note</span>}

src/main/frontend/app/routes/studio/xml-to-json-parser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,8 @@ function extractStickyNotesFromAdapter(adapter: Element, idCounter: IdCounter, f
616616
)
617617

618618
for (const note of notes) {
619-
const text = note.getAttribute('flow:text') ?? ''
619+
const attrText = note.getAttribute('flow:text')
620+
const text = attrText === null ? (note.textContent ?? '') : attrText
620621
const color = note.getAttribute('flow:color') ?? undefined
621622

622623
const x = parseNumericAttribute(note.getAttribute('flow:x'), 0)

src/main/frontend/app/stores/flow-store.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface FlowState {
5555
getAttributes: (nodeId: string) => Record<string, string> | null
5656
addChild: (nodeId: string, child: ChildNode) => void
5757
setStickyText: (nodeId: string, text: string) => void
58+
setStickyHeight: (nodeId: string, height: number) => void
5859
setStickyColor: (nodeId: string, color: string) => void
5960
setStickyCollapsed: (nodeId: string, collapsed: boolean) => void
6061
setStickyAttachment: (nodeId: string, attachedToNodeId: string | null) => void
@@ -342,15 +343,24 @@ const useFlowStore = create<FlowState>()(
342343
},
343344
setStickyText: (nodeId, text) => {
344345
get().saveToHistory()
346+
set({
347+
nodes: get().nodes.map((node) => {
348+
if (node.id === nodeId && isStickyNote(node)) {
349+
return { ...node, data: { ...node.data, content: text } }
350+
}
351+
return node
352+
}),
353+
})
354+
},
355+
setStickyHeight: (nodeId, height) => {
345356
set({
346357
nodes: get().nodes.map((node) => {
347358
if (node.id === nodeId && isStickyNote(node)) {
348359
return {
349360
...node,
350-
data: {
351-
...node.data,
352-
content: text,
353-
},
361+
height,
362+
measured: { ...node.measured, height },
363+
style: { ...node.style, height },
354364
}
355365
}
356366
return node
@@ -378,6 +388,9 @@ const useFlowStore = create<FlowState>()(
378388
const height = node.measured?.height ?? node.height ?? FlowConfig.STICKY_NOTE_DEFAULT_HEIGHT
379389
return {
380390
...node,
391+
width: FlowConfig.STICKY_NOTE_BALLOON_WIDTH,
392+
height: FlowConfig.STICKY_NOTE_BALLOON_HEIGHT,
393+
measured: { width: FlowConfig.STICKY_NOTE_BALLOON_WIDTH, height: FlowConfig.STICKY_NOTE_BALLOON_HEIGHT },
381394
style: {
382395
...node.style,
383396
width: FlowConfig.STICKY_NOTE_BALLOON_WIDTH,
@@ -391,12 +404,17 @@ const useFlowStore = create<FlowState>()(
391404
},
392405
}
393406
} else {
407+
const expandWidth = node.data.preCollapseWidth ?? FlowConfig.STICKY_NOTE_DEFAULT_WIDTH
408+
const expandHeight = node.data.preCollapseHeight ?? FlowConfig.STICKY_NOTE_DEFAULT_HEIGHT
394409
return {
395410
...node,
411+
width: expandWidth,
412+
height: expandHeight,
413+
measured: { width: expandWidth, height: expandHeight },
396414
style: {
397415
...node.style,
398-
width: node.data.preCollapseWidth ?? FlowConfig.STICKY_NOTE_DEFAULT_WIDTH,
399-
height: node.data.preCollapseHeight ?? FlowConfig.STICKY_NOTE_DEFAULT_HEIGHT,
416+
width: expandWidth,
417+
height: expandHeight,
400418
},
401419
data: {
402420
...node.data,

src/main/frontend/src/assets/xsd/FlowConfig.xsd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515

1616
<!-- StickyNote -->
1717
<xs:element name="StickyNote">
18-
<xs:complexType>
18+
<xs:complexType mixed="true">
1919
<xs:attribute name="x" type="xs:int" use="required"/>
2020
<xs:attribute name="y" type="xs:int" use="required"/>
2121
<xs:attribute name="width" type="xs:int" use="required"/>
2222
<xs:attribute name="height" type="xs:int" use="required"/>
23-
<xs:attribute name="text" type="xs:string" use="required"/>
23+
<xs:attribute name="text" type="xs:string"/>
2424
<xs:attribute name="color" type="xs:string"/>
2525
<xs:attribute name="collapsed" type="xs:boolean"/>
2626
<xs:attribute name="attachedTo" type="xs:string"/>

0 commit comments

Comments
 (0)