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
This commit is contained in:
silentoplayz 2026-06-06 08:21:44 -04:00
parent b1d40f3409
commit 3cad442599
2 changed files with 43 additions and 5 deletions

View file

@ -149,10 +149,10 @@ export const Image = Node.create<ImageOptions>({
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 {

View file

@ -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();
}}
/>