Resizable image support

This commit is contained in:
Oliver Walters
2026-05-26 10:27:24 +00:00
parent 36bc444f90
commit c37c1e67af
3 changed files with 23 additions and 47 deletions
+1
View File
@@ -112,6 +112,7 @@
"react-window": "1.8.11", "react-window": "1.8.11",
"recharts": "^3.1.2", "recharts": "^3.1.2",
"styled-components": "^6.1.14", "styled-components": "^6.1.14",
"tiptap-extension-resizable-image": "^2.1.0",
"undici": "^6.24.0", "undici": "^6.24.0",
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
@@ -4,12 +4,13 @@ import '@mantine/tiptap/styles.css';
import { useHotkeys } from '@mantine/hooks'; import { useHotkeys } from '@mantine/hooks';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import Image from '@tiptap/extension-image';
import { useEditor } from '@tiptap/react'; import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit'; import StarterKit from '@tiptap/starter-kit';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { ResizableImage } from 'tiptap-extension-resizable-image';
import 'tiptap-extension-resizable-image/styles.css';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import type { ModelType } from '@lib/enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
@@ -143,51 +144,16 @@ export default function NotesEditor({
StarterKit.configure({ StarterKit.configure({
link: { openOnClick: false } link: { openOnClick: false }
}), }),
Image ResizableImage.configure({
// Paste and drop are handled by the extension's built-in plugin
onUpload: async (file: File) => {
const src = await uploadFileRef.current(file);
return { src, 'data-keep-ratio': true };
}
})
], ],
content: '', content: '',
onUpdate: () => setIsDirty(true), onUpdate: () => setIsDirty(true)
editorProps: {
handleDrop: (view, event) => {
const files = event.dataTransfer?.files;
if (!files?.length) return false;
const imageFiles = Array.from(files).filter((f) =>
f.type.startsWith('image/')
);
if (!imageFiles.length) return false;
event.preventDefault();
const coords = view.posAtCoords({
left: event.clientX,
top: event.clientY
});
imageFiles.forEach((file) => {
uploadFileRef.current(file).then((url) => {
if (!url || !coords) return;
const node = view.state.schema.nodes.image?.create({ src: url });
if (node) view.dispatch(view.state.tr.insert(coords.pos, node));
});
});
return true;
},
handlePaste: (view, event) => {
const files = event.clipboardData?.files;
if (!files?.length) return false;
const imageFiles = Array.from(files).filter((f) =>
f.type.startsWith('image/')
);
if (!imageFiles.length) return false;
imageFiles.forEach((file) => {
uploadFileRef.current(file).then((url) => {
if (!url) return;
const node = view.state.schema.nodes.image?.create({ src: url });
if (node) view.dispatch(view.state.tr.replaceSelectionWith(node));
});
});
return true;
}
}
}); });
// Fetch the available notes for the given model type and ID // Fetch the available notes for the given model type and ID
@@ -360,8 +326,12 @@ export default function NotesEditor({
async (file: File | null) => { async (file: File | null) => {
if (!file || !editor) return; if (!file || !editor) return;
try { try {
const url = await uploadFile(file); const src = await uploadFile(file);
editor.chain().focus().setImage({ src: url }).run(); editor
.chain()
.focus()
.setResizableImage({ src, 'data-keep-ratio': true })
.run();
} catch { } catch {
notifications.show({ notifications.show({
title: t`Error`, title: t`Error`,
@@ -514,7 +484,7 @@ export default function NotesEditor({
color='green' color='green'
leftSection={<IconCirclePlus />} leftSection={<IconCirclePlus />}
onClick={createNote.open} onClick={createNote.open}
disabled={!canEdit || isDirty} // disabled={!canEdit || isDirty}
> >
{t`Add Note`} {t`Add Note`}
</Button> </Button>
+5
View File
@@ -5482,6 +5482,11 @@ tinyglobby@^0.2.13:
fdir "^6.5.0" fdir "^6.5.0"
picomatch "^4.0.4" picomatch "^4.0.4"
tiptap-extension-resizable-image@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiptap-extension-resizable-image/-/tiptap-extension-resizable-image-2.1.0.tgz#26573bcddfeeb06067be6f7c5ace2573b14661b4"
integrity sha512-uvdY/mTHMZ3EF3Z+b61y1htOUg8oF4TiepNFF1pYRg/KITYoYSteF5/L2/qtlZF9SMrzzwnrtRFQFzn/qBj7qQ==
to-regex-range@^5.0.1: to-regex-range@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"