From ec4b797bcd8db98cb5ff38bc311b081b3e3dadde Mon Sep 17 00:00:00 2001 From: Piotr Karamon Date: Tue, 28 Apr 2026 11:52:47 +0200 Subject: [PATCH 01/17] feat: base implementation --- apps/example-web/src/App.tsx | 19 +++ .../example-web/src/components/ImageModal.tsx | 91 +++++++++++ apps/example-web/src/components/Toolbar.tsx | 13 +- .../src/testScreens/VisualRegression.tsx | 7 +- package.json | 1 + src/web/EnrichedTextInput.css | 29 ++++ src/web/EnrichedTextInput.tsx | 7 +- src/web/formats/EnrichedImage.tsx | 145 ++++++++++++++++++ src/web/formats/formatRules.ts | 14 +- src/web/pmPlugins/stripMarksOnImagePlugin.ts | 33 ++++ src/web/useOnChangeState.ts | 6 +- yarn.lock | 10 ++ 12 files changed, 370 insertions(+), 5 deletions(-) create mode 100644 apps/example-web/src/components/ImageModal.tsx create mode 100644 src/web/formats/EnrichedImage.tsx create mode 100644 src/web/pmPlugins/stripMarksOnImagePlugin.ts diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index 864668b48..197b25aae 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -15,6 +15,7 @@ import { WEB_DEFAULT_HTML_STYLE } from './defaultHtmlStyle'; import type { NativeSyntheticEvent } from 'react-native'; import { EditorActions } from './components/EditorActions'; import { SetValueModal } from './components/SetValueModal'; +import { ImageModal } from './components/ImageModal'; import { LinkModal } from './components/LinkModal'; import { HtmlOutputPanel } from './components/HtmlOutputPanel'; import './App.css'; @@ -41,6 +42,7 @@ function App() { const [currentLink, setCurrentLink] = useState(DEFAULT_LINK_STATE); const [isLinkModalOpen, setIsLinkModalOpen] = useState(false); + const [isImageModalOpen, setIsImageModalOpen] = useState(false); const isLinkActive = !!editorState?.link.isActive; const hasLinkUrl = currentLink.url.length > 0; @@ -89,6 +91,18 @@ function App() { setIsLinkModalOpen(false); }; + const openImageModal = () => { + setIsImageModalOpen(true); + }; + + const closeImageModal = () => { + setIsImageModalOpen(false); + }; + + const submitImage = (url: string, width: number, height: number) => { + ref.current?.setImage(url, width, height); + }; + const submitLink = (text: string, url: string) => { if (!selection || url.length === 0) { closeLinkModal(); @@ -140,6 +154,7 @@ function App() { editorRef={ref} state={editorState} onOpenLinkModal={openLinkModal} + onOpenImageModal={openImageModal} /> )} + + {isImageModalOpen && ( + + )} ); } diff --git a/apps/example-web/src/components/ImageModal.tsx b/apps/example-web/src/components/ImageModal.tsx new file mode 100644 index 000000000..0e857dc76 --- /dev/null +++ b/apps/example-web/src/components/ImageModal.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; +import { BaseModal } from './BaseModal'; +import './LinkModal.css'; + +interface ImageModalProps { + onClose: () => void; + onSubmit: (url: string, width: number, height: number) => void; +} + +const DEFAULT_WIDTH = 80; +const DEFAULT_HEIGHT = 80; + +export function ImageModal({ onClose, onSubmit }: ImageModalProps) { + const [width, setWidth] = useState(''); + const [height, setHeight] = useState(''); + const [url, setUrl] = useState(''); + + const reset = () => { + setWidth(''); + setHeight(''); + setUrl(''); + }; + + const handleClose = () => { + reset(); + onClose(); + }; + + const handleSave = () => { + const trimmedUrl = url.trim(); + if (trimmedUrl.length === 0) { + return; + } + const parsedW = parseFloat(width); + const parsedH = parseFloat(height); + const w = Number.isNaN(parsedW) ? DEFAULT_WIDTH : parsedW; + const h = Number.isNaN(parsedH) ? DEFAULT_HEIGHT : parsedH; + onSubmit(trimmedUrl, w, h); + reset(); + onClose(); + }; + + return ( + + { + setUrl(e.target.value); + }} + autoCapitalize="none" + autoCorrect="off" + spellCheck={false} + /> + { + setWidth(e.target.value); + }} + inputMode="decimal" + /> + { + setHeight(e.target.value); + }} + inputMode="decimal" + /> + + + ); +} diff --git a/apps/example-web/src/components/Toolbar.tsx b/apps/example-web/src/components/Toolbar.tsx index dbfe5efbe..91ab0260b 100644 --- a/apps/example-web/src/components/Toolbar.tsx +++ b/apps/example-web/src/components/Toolbar.tsx @@ -10,6 +10,7 @@ interface ToolbarProps { editorRef: RefObject; state: OnChangeStateEvent | null; onOpenLinkModal: () => void; + onOpenImageModal: () => void; } interface ToolbarButtonProps { @@ -52,7 +53,12 @@ function ToolbarButton({ ); } -export function Toolbar({ editorRef, state, onOpenLinkModal }: ToolbarProps) { +export function Toolbar({ + editorRef, + state, + onOpenLinkModal, + onOpenImageModal, +}: ToolbarProps) { const s = state; const dragScroll = useDragScroll(); @@ -156,6 +162,11 @@ export function Toolbar({ editorRef, state, onOpenLinkModal }: ToolbarProps) { label: '🔗', onPress: onOpenLinkModal, }, + { + key: 'image', + label: '🖼', + onPress: onOpenImageModal, + }, { key: 'unorderedList', label: '•', diff --git a/apps/example-web/src/testScreens/VisualRegression.tsx b/apps/example-web/src/testScreens/VisualRegression.tsx index c3f8f692e..9b91c48d0 100644 --- a/apps/example-web/src/testScreens/VisualRegression.tsx +++ b/apps/example-web/src/testScreens/VisualRegression.tsx @@ -80,7 +80,12 @@ export function VisualRegression() { /> - {}} /> + {}} + onOpenImageModal={() => {}} + />