From 3cad442599da603127df7dc92d1c8b1bb57affc1 Mon Sep 17 00:00:00 2001 From: silentoplayz Date: Sat, 6 Jun 2026 08:21:44 -0400 Subject: [PATCH] fix(notes): remove orphaned image nodes when files deleted from controls When an image file is removed from the Controls panel in the note editor, the corresponding image node in the TipTap editor was not being cleaned up. This left behind orphaned nodes that displayed a broken placeholder icon. - Sync editor.storage.files when files are removed from Controls - Walk the editor document and delete image nodes referencing removed files - Fix stale closure in image node view data event listener to read from live editor.storage.files instead of captured-at-creation reference - Dispatch data events to remaining image nodes to refresh their sources --- .../common/RichTextInput/Image/image.ts | 8 ++-- src/lib/components/notes/NoteEditor.svelte | 40 ++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/lib/components/common/RichTextInput/Image/image.ts b/src/lib/components/common/RichTextInput/Image/image.ts index 08e27fc088..791137a2c2 100644 --- a/src/lib/components/common/RichTextInput/Image/image.ts +++ b/src/lib/components/common/RichTextInput/Image/image.ts @@ -149,10 +149,10 @@ export const Image = Node.create({ img.setAttribute('alt', node.attrs.alt || ''); img.setAttribute('title', node.attrs.title || ''); - img.addEventListener('data', (e) => { - const files = e?.files || []; - if (files && node.attrs.src.startsWith('data://')) { - const file = editorFiles.find((f) => f.id === fileId); + img.addEventListener('data', () => { + const currentFiles = editor.storage?.files || []; + if (node.attrs.src.startsWith('data://')) { + const file = currentFiles.find((f) => f.id === fileId); if (file) { img.setAttribute('src', safeImageUrl(file.url || '')); } else { diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte index 4216243857..a3ed457acd 100644 --- a/src/lib/components/notes/NoteEditor.svelte +++ b/src/lib/components/notes/NoteEditor.svelte @@ -1438,7 +1438,45 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, bind:show={showPanel} bind:selectedModelId bind:files - onUpdate={() => { + onUpdate={(updatedFiles) => { + if (editor) { + editor.storage.files = updatedFiles; + + // Remove orphaned image nodes whose files were deleted + const fileIds = new Set(updatedFiles.map((f) => f.id)); + const { state } = editor; + const nodesToRemove = []; + + state.doc.descendants((node, pos) => { + if (node.type.name === 'image' && node.attrs.src?.startsWith('data://')) { + const refId = node.attrs.src.replace('data://', ''); + if (!fileIds.has(refId)) { + nodesToRemove.push({ from: pos, to: pos + node.nodeSize }); + } + } + }); + + if (nodesToRemove.length > 0) { + // Delete in reverse order to preserve positions + let tr = state.tr; + for (let i = nodesToRemove.length - 1; i >= 0; i--) { + tr = tr.delete(nodesToRemove[i].from, nodesToRemove[i].to); + } + editor.view.dispatch(tr); + } + + // Refresh remaining image nodes + for (const file of updatedFiles) { + if (file.type === 'image' || (file?.content_type ?? '').startsWith('image/')) { + const img = document.getElementById(`image:${file.id}`); + if (img) { + img.dispatchEvent(new CustomEvent('data')); + } + } + } + } + + note.data.files = updatedFiles.length > 0 ? updatedFiles : null; changeDebounceHandler(); }} />