Files
windmill/frontend/src/lib/components/graph/renderers/nodes/NoteNode.svelte
T
2025-11-04 16:49:14 +01:00

214 lines
5.5 KiB
Svelte

<script lang="ts">
import { NodeResizer } from '@xyflow/svelte'
import { X } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import GfmMarkdown from '$lib/components/GfmMarkdown.svelte'
import { fade } from 'svelte/transition'
import NoteColorPicker from '../../NoteColorPicker.svelte'
import { NoteColor, NOTE_COLORS, DEFAULT_NOTE_COLOR } from '../../noteColors'
import { Button } from '$lib/components/common'
interface Props {
data: {
text: string
color: NoteColor
onUpdate?: (text: string) => void
onDelete?: () => void
onColorChange?: (color: NoteColor) => void
onSizeChange?: (size: { width: number; height: number }) => void
}
selected?: boolean
dragging?: boolean
}
let { data, selected = false, dragging = false }: Props = $props()
let textareaElement: HTMLTextAreaElement | undefined = $state(undefined)
let editMode = $state(false)
let hovering = $state(false)
let textContent = $state(data.text ?? '')
function handleTextSave() {
// Only update parent when done editing
data.onUpdate?.(textContent)
}
function handleDelete(event?: Event) {
event?.preventDefault?.()
event?.stopPropagation?.()
// Call the delete callback
data.onDelete?.()
}
function handleColorChange(color: NoteColor) {
data.onColorChange?.(color)
}
// Get color configuration for current color
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()
textContent = data.text ?? '' // Initialize local state with current data
editMode = true
// Focus the textarea after a short delay to ensure it's rendered
setTimeout(() => {
textareaElement?.focus()
}, 0)
}
function handleMouseEnter() {
hovering = true
}
function handleMouseLeave() {
hovering = false
}
// Exit edit mode when note is deselected
$effect(() => {
if (!selected && editMode) {
handleTextSave() // Save changes before exiting
editMode = false
}
})
let colorPickerIsOpen = $state(false)
</script>
<div
class={twMerge(
'relative w-full h-full rounded-md group hover:outline outline-1',
colorConfig.background,
colorConfig.text,
colorConfig.outlineHover,
selected ? 'outline' : '',
selected ? colorConfig.outline : '',
editMode ? 'outline-0' : ''
)}
onpointerup={() => {
dragging = false
}}
ondragstart={() => {
dragging = true
}}
ondragend={() => {
dragging = false
}}
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
role="note"
>
<!-- Color picker button -->
<div class="absolute -top-10 -right-2.5 p-2 w-20 h-12 group flex justify-end">
<div
class={twMerge(
'hidden group-hover:flex flex-row gap-2 h-fit',
hovering || editMode || colorPickerIsOpen || selected ? 'flex' : ''
)}
>
<NoteColorPicker
selectedColor={data.color}
onColorChange={handleColorChange}
bind:isOpen={colorPickerIsOpen}
/>
<!-- Delete button -->
<Button
variant="subtle"
unifiedSize="sm"
title="Delete note"
aria-label="Delete note"
startIcon={{ icon: X }}
onClick={handleDelete}
iconOnly
destructive
/>
</div>
</div>
<!-- Hover help text -->
{#if hovering || selected}
{#if !editMode && data.text}
<div
in:fade={{ duration: 200 }}
class="absolute -top-5 h-5 left-0 text-2xs text-secondary rounded-md z-10 transition-opacity duration-300"
>
Double click to edit
</div>
{:else}
<div
in:fade={{ duration: 200 }}
class="absolute -top-5 h-5 left-0 text-2xs text-secondary rounded-md z-10 transition-opacity duration-300"
>GH Markdown</div
>
{/if}
{/if}
<!-- Note content -->
<div class="h-full rounded-md">
{#if editMode}
<!-- Edit mode: show textarea -->
<textarea
bind:this={textareaElement}
bind:value={textContent}
class={twMerge(
'windmillapp w-full h-full min-h-0 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}
spellcheck="false"
></textarea>
{:else}
<!-- Render mode: show markdown or empty state -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={twMerge(
'w-full h-full overflow-auto cursor-pointer flex items-start justify-center rounded-md p-4'
)}
ondblclick={handleDoubleClick}
>
{#if data.text}
<div class={twMerge('w-full h-full text-xs rounded-md', colorConfig.text)}>
<GfmMarkdown md={data.text} noPadding />
</div>
{:else}
<div class={twMerge('text-xs italic opacity-60', colorConfig.text)}>
Double-click to add a note
</div>
{/if}
</div>
{/if}
</div>
<!-- Node resizer - only visible when selected -->
<NodeResizer
isVisible={selected && !dragging}
minWidth={200}
minHeight={100}
lineClass="!border-4 !border-transparent !rounded-md"
handleClass="!bg-transparent !w-4 !h-4 !border-none !rounded-md"
onResizeEnd={(_, params) => {
// Update note size when resizing ends
if (data.onSizeChange && params.width !== undefined && params.height !== undefined) {
data.onSizeChange({ width: params.width, height: params.height })
}
}}
/>
</div>
<style>
textarea::placeholder {
color: #6b7280;
opacity: 0.7;
}
/* Remove default textarea styling */
textarea {
font-family: inherit;
line-height: 1.4;
}
</style>