mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
clean
This commit is contained in:
@@ -196,6 +196,13 @@ pub struct FlowValue {
|
||||
pub notes: Option<Vec<FlowNote>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FlowNoteType {
|
||||
Free,
|
||||
Group,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct FlowNote {
|
||||
pub id: String,
|
||||
@@ -203,6 +210,12 @@ pub struct FlowNote {
|
||||
pub position: FlowNotePosition,
|
||||
pub size: FlowNoteSize,
|
||||
pub color: String,
|
||||
#[serde(rename = "type")]
|
||||
pub note_type: FlowNoteType,
|
||||
#[serde(default)]
|
||||
pub locked: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub contained_node_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
|
||||
@@ -221,6 +221,9 @@
|
||||
// Initialize note manager (now stateless)
|
||||
const noteManager = new NoteManager()
|
||||
|
||||
// Runtime text height tracking for notes (not stored in FlowNote)
|
||||
let noteTextHeights = $state<Record<string, number>>({})
|
||||
|
||||
// Selection manager - create one if not provided
|
||||
let actualSelectionManager = selectionManager || new SelectionManager()
|
||||
|
||||
@@ -339,7 +342,8 @@
|
||||
const groupNoteHeight = noteManager.getGroupNoteHeightForNode(
|
||||
notes ?? [],
|
||||
node.id,
|
||||
initialNodes
|
||||
initialNodes,
|
||||
noteTextHeights
|
||||
)
|
||||
if (groupNoteHeight > 0) {
|
||||
spacingMap.set(node.id, groupNoteHeight)
|
||||
@@ -527,9 +531,17 @@
|
||||
|
||||
nodes = [
|
||||
...finalNodes,
|
||||
...noteManager.convertToNodes(notes ?? [], finalNodes, (newNotes) => {
|
||||
notes = newNotes
|
||||
})
|
||||
...noteManager.convertToNodes(
|
||||
notes ?? [],
|
||||
finalNodes,
|
||||
noteTextHeights,
|
||||
(newNotes) => {
|
||||
notes = newNotes
|
||||
},
|
||||
(noteId, height) => {
|
||||
noteTextHeights[noteId] = height
|
||||
}
|
||||
)
|
||||
]
|
||||
edges = [
|
||||
...(assetNodesResult?.newAssetEdges ?? []),
|
||||
@@ -708,6 +720,7 @@
|
||||
}
|
||||
|
||||
$inspect('dbg notes & nodes', notes, nodes)
|
||||
$inspect('dbg noteTextHeights', noteTextHeights)
|
||||
</script>
|
||||
|
||||
{#if insertable}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Node } from '@xyflow/svelte'
|
||||
import { useSvelteFlow } from '@xyflow/svelte'
|
||||
import { NODE } from './util'
|
||||
import { calculateNodesBounds } from './util'
|
||||
|
||||
interface Props {
|
||||
selectedNodes: Node[]
|
||||
@@ -17,17 +17,7 @@
|
||||
}
|
||||
|
||||
// Calculate flow coordinates bounds
|
||||
let minX = Infinity
|
||||
let maxX = -Infinity
|
||||
let minY = Infinity
|
||||
let maxY = -Infinity
|
||||
|
||||
selectedNodes.forEach(node => {
|
||||
minX = Math.min(minX, node.position.x)
|
||||
maxX = Math.max(maxX, node.position.x + NODE.width)
|
||||
minY = Math.min(minY, node.position.y)
|
||||
maxY = Math.max(maxY, node.position.y + NODE.height)
|
||||
})
|
||||
const { minX, minY, maxX, maxY } = calculateNodesBounds(selectedNodes)
|
||||
|
||||
// Add padding in flow coordinates
|
||||
const flowBounds = {
|
||||
@@ -78,8 +68,10 @@
|
||||
z-index: 10;
|
||||
"
|
||||
>
|
||||
<div class="absolute -top-6 left-0 text-xs text-accent font-medium bg-surface px-2 py-1 rounded shadow pointer-events-none">
|
||||
<div
|
||||
class="absolute -top-6 left-0 text-xs text-accent font-medium bg-surface px-2 py-1 rounded shadow pointer-events-none"
|
||||
>
|
||||
{selectedNodes.length} nodes selected
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -9,33 +9,6 @@ export interface GroupNoteBounds {
|
||||
height: number
|
||||
}
|
||||
|
||||
// Extended types for group notes
|
||||
export interface RegularNote extends FlowNote {
|
||||
type: 'regular'
|
||||
}
|
||||
|
||||
export interface GroupNote extends Omit<FlowNote, 'position' | 'size'> {
|
||||
type: 'group'
|
||||
containedNodeIds: string[]
|
||||
// position and size will be calculated dynamically
|
||||
}
|
||||
|
||||
export type ExtendedFlowNote = RegularNote | GroupNote
|
||||
|
||||
/**
|
||||
* Type guard to check if a note is a group note
|
||||
*/
|
||||
export function isGroupNote(note: ExtendedFlowNote): note is GroupNote {
|
||||
return note.type === 'group'
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a note is a regular note
|
||||
*/
|
||||
export function isRegularNote(note: ExtendedFlowNote): note is RegularNote {
|
||||
return note.type === 'regular'
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the bounding box that wraps all contained nodes with padding
|
||||
* Uses the same calculation logic as SelectionBoundingBox for consistency
|
||||
@@ -90,116 +63,19 @@ export function getTopMostNode(containedNodeIds: string[], nodes: Node[]): Node
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dynamic bounds for a group note
|
||||
*/
|
||||
export function calculateGroupNoteBounds(
|
||||
groupNote: GroupNote,
|
||||
nodes: Node[],
|
||||
textHeight: number = 60
|
||||
): { position: { x: number; y: number }; size: { width: number; height: number } } {
|
||||
const bounds = computeGroupNoteBounds(groupNote.containedNodeIds, nodes, textHeight)
|
||||
|
||||
return {
|
||||
position: {
|
||||
x: bounds.x,
|
||||
y: bounds.y
|
||||
},
|
||||
size: {
|
||||
width: bounds.width,
|
||||
height: bounds.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new group note with contained node IDs instead of fixed bounds
|
||||
*/
|
||||
export function createGroupNote(
|
||||
selectedNodeIds: string[],
|
||||
color: string = 'yellow'
|
||||
): GroupNote {
|
||||
if (selectedNodeIds.length === 0) {
|
||||
throw new Error('No nodes selected for group note')
|
||||
}
|
||||
|
||||
return {
|
||||
id: `group-note-${Date.now()}`,
|
||||
type: 'group',
|
||||
text: `Group note for ${selectedNodeIds.length} nodes`,
|
||||
containedNodeIds: [...selectedNodeIds], // Copy array to avoid mutation
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a node to an existing group note
|
||||
*/
|
||||
export function addNodeToGroup(groupNote: GroupNote, nodeId: string): GroupNote {
|
||||
if (groupNote.containedNodeIds.includes(nodeId)) {
|
||||
return groupNote // Node already in group
|
||||
}
|
||||
|
||||
return {
|
||||
...groupNote,
|
||||
containedNodeIds: [...groupNote.containedNodeIds, nodeId],
|
||||
text: `Group note for ${groupNote.containedNodeIds.length + 1} nodes`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a node from an existing group note
|
||||
*/
|
||||
export function removeNodeFromGroup(groupNote: GroupNote, nodeId: string): GroupNote | null {
|
||||
const updatedNodeIds = groupNote.containedNodeIds.filter(id => id !== nodeId)
|
||||
|
||||
if (updatedNodeIds.length === 0) {
|
||||
return null // Group note should be deleted if no nodes remain
|
||||
}
|
||||
|
||||
return {
|
||||
...groupNote,
|
||||
containedNodeIds: updatedNodeIds,
|
||||
text: `Group note for ${updatedNodeIds.length} nodes`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that all contained nodes exist in the provided nodes array
|
||||
*/
|
||||
export function validateGroupNote(groupNote: GroupNote, nodes: Node[]): GroupNote {
|
||||
export function validateGroupNote(note: FlowNote, nodes: Node[]): FlowNote {
|
||||
if (note.type !== 'group' || !note.contained_node_ids) {
|
||||
return note
|
||||
}
|
||||
|
||||
const validNodeIds = nodes.map(node => node.id)
|
||||
const validContainedNodeIds = groupNote.containedNodeIds.filter(id => validNodeIds.includes(id))
|
||||
const validContainedNodeIds = note.contained_node_ids.filter(id => validNodeIds.includes(id))
|
||||
|
||||
return {
|
||||
...groupNote,
|
||||
containedNodeIds: validContainedNodeIds,
|
||||
text: `Group note for ${validContainedNodeIds.length} nodes`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a legacy FlowNote to the new ExtendedFlowNote format
|
||||
*/
|
||||
export function convertToExtendedNote(note: FlowNote & { isGroupNote?: boolean; containedNodeIds?: string[] }): ExtendedFlowNote {
|
||||
if (note.isGroupNote && note.containedNodeIds) {
|
||||
// Convert legacy group note
|
||||
return {
|
||||
id: note.id,
|
||||
type: 'group',
|
||||
text: note.text,
|
||||
color: note.color,
|
||||
containedNodeIds: note.containedNodeIds
|
||||
}
|
||||
} else {
|
||||
// Convert regular note
|
||||
return {
|
||||
id: note.id,
|
||||
type: 'regular',
|
||||
text: note.text,
|
||||
position: note.position,
|
||||
size: note.size,
|
||||
color: note.color
|
||||
}
|
||||
...note,
|
||||
contained_node_ids: validContainedNodeIds
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { FlowNote } from '$lib/gen'
|
||||
import type { Node } from '@xyflow/svelte'
|
||||
import type { NoteColor } from './noteColors'
|
||||
import {
|
||||
createGroupNote,
|
||||
isGroupNote,
|
||||
calculateGroupNoteBounds,
|
||||
convertToExtendedNote
|
||||
} from './groupNoteUtils'
|
||||
import { calculateNodesBounds } from './util'
|
||||
|
||||
type NodeDep = { id: string; parentIds?: string[]; offset?: number }
|
||||
type NodePos = { position: { x: number; y: number } }
|
||||
@@ -29,7 +24,9 @@ export class NoteManager {
|
||||
text: newNoteFromTool.data?.text || '',
|
||||
position: newNoteFromTool.position,
|
||||
size: { width: newNoteFromTool.width || 300, height: newNoteFromTool.height || 100 },
|
||||
color: newNoteFromTool.data?.color || 'yellow'
|
||||
color: newNoteFromTool.data?.color || 'yellow',
|
||||
type: 'free',
|
||||
locked: false
|
||||
}
|
||||
return [...notes, newNote]
|
||||
}
|
||||
@@ -83,7 +80,7 @@ export class NoteManager {
|
||||
* Update note lock state
|
||||
*/
|
||||
updateLock(notes: FlowNote[], noteId: string, locked: boolean): FlowNote[] {
|
||||
return notes.map((note) => (note.id === noteId ? ({ ...note, locked } as any) : note))
|
||||
return notes.map((note) => (note.id === noteId ? { ...note, locked } : note))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,46 +89,70 @@ export class NoteManager {
|
||||
createGroupNote(notes: FlowNote[], selectedNodeIds: string[]): FlowNote[] {
|
||||
if (selectedNodeIds.length === 0) return notes
|
||||
|
||||
try {
|
||||
const groupNote = createGroupNote(selectedNodeIds)
|
||||
// For now, we need to store group notes as FlowNote format with additional properties
|
||||
// We'll add dummy position/size that will be calculated dynamically in convertNotesToNodes
|
||||
const newGroupNote = {
|
||||
...groupNote,
|
||||
position: { x: 0, y: 0 }, // Dummy values, will be calculated dynamically
|
||||
size: { width: 300, height: 100 }, // Dummy values, will be calculated dynamically
|
||||
locked: false, // Group notes are not locked, just not movable/resizable
|
||||
isGroupNote: true,
|
||||
containedNodeIds: groupNote.containedNodeIds,
|
||||
type: 'group'
|
||||
} as FlowNote & {
|
||||
locked: boolean
|
||||
isGroupNote: boolean
|
||||
containedNodeIds: string[]
|
||||
type: string
|
||||
const newGroupNote: FlowNote = {
|
||||
id: `group-${Date.now()}`,
|
||||
text: '',
|
||||
position: { x: 0, y: 0 }, // Will be calculated dynamically
|
||||
size: { width: 300, height: 100 }, // Will be calculated dynamically
|
||||
color: 'gray',
|
||||
type: 'group',
|
||||
locked: false,
|
||||
contained_node_ids: selectedNodeIds
|
||||
}
|
||||
return [...notes, newGroupNote]
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate position and size for group notes based on contained nodes
|
||||
*/
|
||||
calculateGroupNoteLayout(
|
||||
note: FlowNote,
|
||||
nodes: Node[],
|
||||
textHeight: number = 60
|
||||
): { position: { x: number; y: number }; size: { width: number; height: number } } {
|
||||
if (note.type !== 'group' || !note.contained_node_ids?.length) {
|
||||
return { position: note.position, size: note.size }
|
||||
}
|
||||
|
||||
const containedNodes = nodes.filter((node) => note.contained_node_ids?.includes(node.id))
|
||||
|
||||
if (containedNodes.length === 0) {
|
||||
return { position: note.position, size: note.size }
|
||||
}
|
||||
|
||||
// Find bounds of all contained nodes
|
||||
const bounds = calculateNodesBounds(containedNodes)
|
||||
|
||||
const padding = 20
|
||||
return {
|
||||
position: {
|
||||
x: bounds.minX - padding,
|
||||
y: bounds.minY - textHeight - padding
|
||||
},
|
||||
size: {
|
||||
width: bounds.maxX - bounds.minX + 2 * padding,
|
||||
height: bounds.maxY - bounds.minY + textHeight + 2 * padding
|
||||
}
|
||||
return [...notes, newGroupNote]
|
||||
} catch (error) {
|
||||
console.error('Failed to create group note:', error)
|
||||
return notes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine if a node needs additional spacing above it for group notes.
|
||||
* Returns the height needed above the node.
|
||||
* Returns the height needed above the node (text height + padding).
|
||||
*/
|
||||
getGroupNoteHeightForNode(
|
||||
notes: FlowNote[],
|
||||
nodeId: string,
|
||||
layoutedNodes: (NodeDep & NodePos)[]
|
||||
layoutedNodes: (NodeDep & NodePos)[],
|
||||
noteTextHeights: Record<string, number>
|
||||
): number {
|
||||
const PADDING = 20 // Fixed padding above and below the note text
|
||||
|
||||
for (const note of notes) {
|
||||
const extendedNote = convertToExtendedNote(note as any)
|
||||
if (isGroupNote(extendedNote) && extendedNote.containedNodeIds.includes(nodeId)) {
|
||||
if (note.type === 'group' && note.contained_node_ids?.includes(nodeId)) {
|
||||
// Find the topmost node in this group by Y position
|
||||
const containedNodes = layoutedNodes.filter((node) =>
|
||||
extendedNote.containedNodeIds.includes(node.id)
|
||||
note.contained_node_ids?.includes(node.id)
|
||||
)
|
||||
|
||||
if (containedNodes.length > 0) {
|
||||
@@ -141,7 +162,9 @@ export class NoteManager {
|
||||
|
||||
// If this is the topmost node in the group, return the needed height
|
||||
if (topmostNode.id === nodeId) {
|
||||
return 60 // Height for group note text
|
||||
// Use actual text height if available, otherwise default to 60
|
||||
const textHeight = noteTextHeights[note.id] || 60
|
||||
return textHeight + PADDING
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,98 +172,77 @@ export class NoteManager {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Create common data object for note nodes
|
||||
*/
|
||||
private createNoteData(
|
||||
note: FlowNote,
|
||||
notes: FlowNote[],
|
||||
onNotesChange: (notes: FlowNote[]) => void,
|
||||
onTextHeightChange: (noteId: string, height: number) => void,
|
||||
isGroupNote: boolean
|
||||
) {
|
||||
return {
|
||||
text: note.text,
|
||||
color: note.color,
|
||||
locked: note.locked || false,
|
||||
isGroupNote,
|
||||
...(isGroupNote && { containedNodeIds: note.contained_node_ids || [] }),
|
||||
onUpdate: (text: string) => {
|
||||
const newNotes = this.updateText(notes, note.id, text)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onDelete: () => {
|
||||
const newNotes = this.delete(notes, note.id)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onColorChange: (color: NoteColor) => {
|
||||
const newNotes = this.updateColor(notes, note.id, color)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onSizeChange: (size: { width: number; height: number }) => {
|
||||
const newNotes = this.updateSize(notes, note.id, size)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onLockToggle: (locked: boolean) => {
|
||||
const newNotes = this.updateLock(notes, note.id, locked)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onTextHeightChange: (textHeight: number) => {
|
||||
onTextHeightChange(note.id, textHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert notes to SvelteFlow nodes
|
||||
*/
|
||||
convertToNodes(
|
||||
notes: FlowNote[],
|
||||
currentNodes: Node[],
|
||||
onNotesChange: (notes: FlowNote[]) => void
|
||||
textHeights: Record<string, number>,
|
||||
onNotesChange: (notes: FlowNote[]) => void,
|
||||
onTextHeightChange: (noteId: string, height: number) => void
|
||||
): Node[] {
|
||||
return notes.map((note) => {
|
||||
const extendedNote = convertToExtendedNote(note as any)
|
||||
const isGroupNote = note.type === 'group'
|
||||
|
||||
if (isGroupNote(extendedNote)) {
|
||||
// Calculate dynamic bounds for group notes
|
||||
const bounds = calculateGroupNoteBounds(extendedNote, currentNodes, 60)
|
||||
// Calculate position and size based on note type
|
||||
const { position, size } = isGroupNote
|
||||
? this.calculateGroupNoteLayout(note, currentNodes, textHeights[note.id] || 60)
|
||||
: { position: note.position, size: note.size }
|
||||
|
||||
return {
|
||||
id: extendedNote.id,
|
||||
type: 'note',
|
||||
position: bounds.position,
|
||||
data: {
|
||||
text: extendedNote.text,
|
||||
color: extendedNote.color,
|
||||
locked: false, // Group notes are not locked - they can be edited
|
||||
isGroupNote: true,
|
||||
containedNodeIds: extendedNote.containedNodeIds,
|
||||
onUpdate: (text: string) => {
|
||||
const newNotes = this.updateText(notes, extendedNote.id, text)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onDelete: () => {
|
||||
const newNotes = this.delete(notes, extendedNote.id)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onColorChange: (color: NoteColor) => {
|
||||
const newNotes = this.updateColor(notes, extendedNote.id, color)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onSizeChange: (size: { width: number; height: number }) => {
|
||||
const newNotes = this.updateSize(notes, extendedNote.id, size)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onLockToggle: (locked: boolean) => {
|
||||
const newNotes = this.updateLock(notes, extendedNote.id, locked)
|
||||
onNotesChange(newNotes)
|
||||
}
|
||||
},
|
||||
style: `width: ${bounds.size.width}px; height: ${bounds.size.height}px;`,
|
||||
width: bounds.size.width,
|
||||
height: bounds.size.height,
|
||||
zIndex: -2000,
|
||||
draggable: false, // Group notes cannot be moved - position is determined by contained nodes
|
||||
selectable: true
|
||||
}
|
||||
} else {
|
||||
// Handle regular notes
|
||||
return {
|
||||
id: extendedNote.id,
|
||||
type: 'note',
|
||||
position: extendedNote.position,
|
||||
data: {
|
||||
text: extendedNote.text,
|
||||
color: extendedNote.color,
|
||||
locked: (note as any).locked || false,
|
||||
isGroupNote: false,
|
||||
onUpdate: (text: string) => {
|
||||
const newNotes = this.updateText(notes, extendedNote.id, text)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onDelete: () => {
|
||||
const newNotes = this.delete(notes, extendedNote.id)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onColorChange: (color: NoteColor) => {
|
||||
const newNotes = this.updateColor(notes, extendedNote.id, color)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onSizeChange: (size: { width: number; height: number }) => {
|
||||
const newNotes = this.updateSize(notes, extendedNote.id, size)
|
||||
onNotesChange(newNotes)
|
||||
},
|
||||
onLockToggle: (locked: boolean) => {
|
||||
const newNotes = this.updateLock(notes, extendedNote.id, locked)
|
||||
onNotesChange(newNotes)
|
||||
}
|
||||
},
|
||||
style: `width: ${extendedNote.size.width}px; height: ${extendedNote.size.height}px;`,
|
||||
width: extendedNote.size.width,
|
||||
height: extendedNote.size.height,
|
||||
zIndex: -2000,
|
||||
draggable: !(note as any).locked, // Don't allow dragging locked notes
|
||||
selectable: true
|
||||
}
|
||||
return {
|
||||
id: note.id,
|
||||
type: 'note',
|
||||
position,
|
||||
data: this.createNoteData(note, notes, onNotesChange, onTextHeightChange, isGroupNote),
|
||||
style: `width: ${size.width}px; height: ${size.height}px;`,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
zIndex: -2000,
|
||||
draggable: isGroupNote ? false : !note.locked,
|
||||
selectable: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
onColorChange?: (color: NoteColor) => void
|
||||
onSizeChange?: (size: { width: number; height: number }) => void
|
||||
onLockToggle?: (locked: boolean) => void
|
||||
onTextHeightChange?: (height: number) => void
|
||||
}
|
||||
selected?: boolean
|
||||
dragging?: boolean
|
||||
@@ -27,57 +28,14 @@
|
||||
let { data, selected = false, dragging = false }: Props = $props()
|
||||
|
||||
let textareaElement: HTMLTextAreaElement | undefined = $state(undefined)
|
||||
let contentElement: HTMLDivElement | undefined = $state(undefined)
|
||||
let editMode = $state(false)
|
||||
let hovering = $state(false)
|
||||
let textContent = $state(data.text ?? '')
|
||||
let contentHeight = $state(0)
|
||||
|
||||
function calculateContentHeight() {
|
||||
let calculatedHeight = 0
|
||||
let currentWidth = 300 // Default width
|
||||
|
||||
if (editMode && textareaElement) {
|
||||
// For textarea in edit mode
|
||||
textareaElement.style.height = 'auto'
|
||||
const scrollHeight = textareaElement.scrollHeight
|
||||
const minHeight = 60 // Minimum height in pixels
|
||||
const maxHeight = 400 // Maximum height in pixels
|
||||
calculatedHeight = Math.max(minHeight, Math.min(maxHeight, scrollHeight))
|
||||
textareaElement.style.height = `${calculatedHeight}px`
|
||||
|
||||
// Get current width from parent element
|
||||
const parentElement = textareaElement.closest('.svelte-flow__node')
|
||||
if (parentElement) {
|
||||
currentWidth = parentElement.getBoundingClientRect().width
|
||||
}
|
||||
} else if (!editMode && contentElement) {
|
||||
// For content in display mode
|
||||
const scrollHeight = contentElement.scrollHeight
|
||||
const minHeight = 60
|
||||
const maxHeight = 400
|
||||
calculatedHeight = Math.max(minHeight, Math.min(maxHeight, scrollHeight))
|
||||
|
||||
// Get current width from parent element
|
||||
const parentElement = contentElement.closest('.svelte-flow__node')
|
||||
if (parentElement) {
|
||||
currentWidth = parentElement.getBoundingClientRect().width
|
||||
}
|
||||
}
|
||||
|
||||
contentHeight = calculatedHeight
|
||||
|
||||
// Update note size if handler is available
|
||||
if (data.onSizeChange && contentHeight > 0) {
|
||||
data.onSizeChange({ width: currentWidth, height: contentHeight + 40 }) // Add extra padding for note UI
|
||||
}
|
||||
}
|
||||
let containerHeight = $state(0)
|
||||
|
||||
function handleTextSave() {
|
||||
// Only update parent when done editing
|
||||
data.onUpdate?.(textContent)
|
||||
// Recalculate height after saving
|
||||
setTimeout(calculateContentHeight, 0)
|
||||
}
|
||||
|
||||
function handleDelete(event?: Event) {
|
||||
@@ -101,7 +59,6 @@
|
||||
const colorConfig = $derived(NOTE_COLORS[data.color] || NOTE_COLORS[DEFAULT_NOTE_COLOR])
|
||||
|
||||
function handleDoubleClick(event: Event) {
|
||||
console.log('Double click detected', { editMode, selected, dragging })
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -115,7 +72,6 @@
|
||||
// Focus the textarea after a short delay to ensure it's rendered
|
||||
setTimeout(() => {
|
||||
textareaElement?.focus()
|
||||
calculateContentHeight()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
@@ -135,18 +91,10 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Auto-resize when content changes
|
||||
// Track content height and notify parent
|
||||
$effect(() => {
|
||||
textContent // Track textContent changes
|
||||
if (editMode || contentElement) {
|
||||
setTimeout(calculateContentHeight, 0)
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate initial content height
|
||||
$effect(() => {
|
||||
if (data.text && !editMode && contentElement) {
|
||||
setTimeout(calculateContentHeight, 0)
|
||||
if (containerHeight > 0) {
|
||||
data.onTextHeightChange?.(containerHeight)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -239,34 +187,40 @@
|
||||
{/if}
|
||||
|
||||
<!-- Note content -->
|
||||
<div class="w-full min-h-[60px] max-h-[400px] rounded-md">
|
||||
<div
|
||||
bind:clientHeight={containerHeight}
|
||||
class="w-full min-h-[60px] max-h-[400px] h-fit rounded-md"
|
||||
>
|
||||
{#if editMode}
|
||||
<!-- Edit mode: show textarea -->
|
||||
<textarea
|
||||
bind:this={textareaElement}
|
||||
bind:value={textContent}
|
||||
class={twMerge(
|
||||
'windmillapp w-full min-h-[60px] max-h-[400px] shadow-none resize-none text-xs overflow-y-auto border-none rounded-md bg-transparent transition-colors p-4',
|
||||
'windmillapp w-full h-auto min-h-[60px] max-h-[400px] shadow-none resize-none text-xs overflow-y-auto border-none rounded-md bg-transparent transition-colors p-4',
|
||||
colorConfig.text
|
||||
)}
|
||||
placeholder="Add your note here... (Markdown supported)"
|
||||
onblur={handleTextSave}
|
||||
oninput={calculateContentHeight}
|
||||
spellcheck="false"
|
||||
style="height: auto;"
|
||||
style="field-sizing: content;"
|
||||
></textarea>
|
||||
{:else}
|
||||
<!-- Render mode: show markdown or empty state -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={contentElement}
|
||||
class={twMerge(
|
||||
'w-full min-h-[60px] max-h-[400px] overflow-auto cursor-pointer flex items-start justify-start rounded-md p-4'
|
||||
'w-full h-fit min-h-[60px] max-h-[400px] overflow-auto cursor-pointer flex items-start justify-start rounded-md p-4'
|
||||
)}
|
||||
ondblclick={handleDoubleClick}
|
||||
>
|
||||
{#if data.text}
|
||||
<div class={twMerge('w-full text-xs rounded-md', colorConfig.text)}>
|
||||
<div
|
||||
class={twMerge(
|
||||
'w-full text-xs rounded-md break-words overflow-hidden',
|
||||
colorConfig.text
|
||||
)}
|
||||
>
|
||||
<GfmMarkdown md={data.text} noPadding />
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -126,3 +126,27 @@ export function getNodeColorClasses(state: FlowNodeState, selected: boolean): Fl
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the bounding box for a collection of nodes
|
||||
* @param nodes - Array of nodes with position.x and position.y properties
|
||||
* @returns The bounds { minX, minY, maxX, maxY }
|
||||
*/
|
||||
export function calculateNodesBounds(
|
||||
nodes: Array<{ position: { x: number; y: number } }>
|
||||
): { minX: number; minY: number; maxX: number; maxY: number } {
|
||||
return nodes.reduce(
|
||||
(acc, node) => ({
|
||||
minX: Math.min(acc.minX, node.position.x),
|
||||
minY: Math.min(acc.minY, node.position.y),
|
||||
maxX: Math.max(acc.maxX, node.position.x + NODE.width),
|
||||
maxY: Math.max(acc.maxY, node.position.y + NODE.height)
|
||||
}),
|
||||
{
|
||||
minX: Infinity,
|
||||
minY: Infinity,
|
||||
maxX: -Infinity,
|
||||
maxY: -Infinity
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -143,12 +143,26 @@ components:
|
||||
color:
|
||||
type: string
|
||||
description: Color of the note (e.g., "yellow", "#ffff00")
|
||||
type:
|
||||
type: string
|
||||
enum: [free, group]
|
||||
description: Type of note - 'free' for standalone notes, 'group' for notes that group other nodes
|
||||
locked:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the note is locked and cannot be edited or moved
|
||||
contained_node_ids:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: For group notes, the IDs of nodes contained within this group
|
||||
required:
|
||||
- id
|
||||
- text
|
||||
- position
|
||||
- size
|
||||
- color
|
||||
- type
|
||||
|
||||
RetryIf:
|
||||
type: object
|
||||
|
||||
Reference in New Issue
Block a user