From 1ca4924e9655d66aac87ace93d473c23aeb124fd Mon Sep 17 00:00:00 2001 From: Petyo Ivanov Date: Thu, 5 Oct 2023 17:51:46 +0300 Subject: [PATCH] feat: image dialog supports upload Various improvements across dialogs Fixes #105 --- src/examples/link-dialog.tsx | 33 +++- src/plugins/core/PropertyPopover.tsx | 7 +- src/plugins/core/ui/DownshiftAutoComplete.tsx | 90 ++++++++++ src/plugins/frontmatter/FrontmatterEditor.tsx | 14 +- src/plugins/image/ImageDialog.tsx | 96 ++++++++++ src/plugins/image/ImageEditor.tsx | 22 ++- src/plugins/image/ImageNode.tsx | 10 +- src/plugins/image/index.ts | 104 ++++++++--- src/plugins/link-dialog/LinkDialog.tsx | 157 +++++----------- src/plugins/link-dialog/index.ts | 75 +++----- .../toolbar/components/InsertImage.tsx | 168 +----------------- .../toolbar/primitives/DialogButton.tsx | 2 +- src/styles/ui.module.css | 14 ++ 13 files changed, 438 insertions(+), 354 deletions(-) create mode 100644 src/plugins/core/ui/DownshiftAutoComplete.tsx create mode 100644 src/plugins/image/ImageDialog.tsx diff --git a/src/examples/link-dialog.tsx b/src/examples/link-dialog.tsx index ac05208e..219ab40d 100644 --- a/src/examples/link-dialog.tsx +++ b/src/examples/link-dialog.tsx @@ -7,7 +7,9 @@ import { directivesPlugin, headingsPlugin, quotePlugin, - listsPlugin + listsPlugin, + toolbarPlugin, + CreateLink } from '../' import admonitionMarkdown from './assets/admonition.md?raw' @@ -49,3 +51,32 @@ export function ParentOffsetOfAnchor() { ) } + +export function EditorInAForm() { + return ( +
+
{ + evt.preventDefault() + alert('main form submitted') + }} + > + ( + <> + + + ) + }) + ]} + /> + + +
+ ) +} diff --git a/src/plugins/core/PropertyPopover.tsx b/src/plugins/core/PropertyPopover.tsx index fddafed0..05a17067 100644 --- a/src/plugins/core/PropertyPopover.tsx +++ b/src/plugins/core/PropertyPopover.tsx @@ -42,10 +42,11 @@ export const PropertyPopover: React.FC = ({ title, propert
{ - onChange(values) + onSubmit={(e) => { + void handleSubmit(onChange)(e) setOpen(false) - })} + e.nativeEvent.stopImmediatePropagation() + }} >

{title} Attributes

diff --git a/src/plugins/core/ui/DownshiftAutoComplete.tsx b/src/plugins/core/ui/DownshiftAutoComplete.tsx new file mode 100644 index 00000000..b7ab9c5f --- /dev/null +++ b/src/plugins/core/ui/DownshiftAutoComplete.tsx @@ -0,0 +1,90 @@ +import { useCombobox } from 'downshift' +import React from 'react' +import { Control, UseFormSetValue, Controller } from 'react-hook-form' +import styles from '../../../styles/ui.module.css' +import DropDownIcon from '../../../icons/arrow_drop_down.svg' + +const MAX_SUGGESTIONS = 20 + +export const DownshiftAutoComplete: React.FC<{ + suggestions: string[] + control: Control + setValue: UseFormSetValue + placeholder: string + inputName: string + autofocus?: boolean + initialInputValue: string +}> = ({ autofocus, suggestions, control, inputName, placeholder, initialInputValue, setValue }) => { + const [items, setItems] = React.useState(suggestions.slice(0, MAX_SUGGESTIONS)) + + const enableAutoComplete = suggestions.length > 0 + + const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, highlightedIndex, getItemProps, selectedItem } = useCombobox({ + initialInputValue, + onInputValueChange({ inputValue }) { + inputValue = inputValue?.toLowerCase() || '' + setValue(inputName, inputValue) + const matchingItems = [] + for (const suggestion of suggestions) { + if (suggestion.toLowerCase().includes(inputValue)) { + matchingItems.push(suggestion) + if (matchingItems.length >= MAX_SUGGESTIONS) { + break + } + } + } + setItems(matchingItems) + }, + items, + itemToString(item) { + return item ?? '' + } + }) + + const dropdownIsVisible = isOpen && items.length > 0 + return ( +
+
+ { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const downshiftSrcProps = getInputProps() + return ( + + ) + }} + /> + {enableAutoComplete && ( + + )} +
+ +
+
    + {items.map((item, index: number) => ( +
  • + {item} +
  • + ))} +
+
+
+ ) +} diff --git a/src/plugins/frontmatter/FrontmatterEditor.tsx b/src/plugins/frontmatter/FrontmatterEditor.tsx index e44b94d0..a0980146 100644 --- a/src/plugins/frontmatter/FrontmatterEditor.tsx +++ b/src/plugins/frontmatter/FrontmatterEditor.tsx @@ -65,7 +65,13 @@ export const FrontmatterEditor = ({ yaml, onChange }: FrontmatterEditorProps) => Edit document frontmatter - setFrontmatterDialogOpen(false)}> + { + void handleSubmit(onSubmit)(e) + e.nativeEvent.stopImmediatePropagation() + }} + onReset={() => setFrontmatterDialogOpen(false)} + >
@@ -116,12 +122,12 @@ export const FrontmatterEditor = ({ yaml, onChange }: FrontmatterEditorProps) =>
- +
diff --git a/src/plugins/image/ImageDialog.tsx b/src/plugins/image/ImageDialog.tsx new file mode 100644 index 00000000..605420cc --- /dev/null +++ b/src/plugins/image/ImageDialog.tsx @@ -0,0 +1,96 @@ +import * as Dialog from '@radix-ui/react-dialog' +import classNames from 'classnames' +import React from 'react' +import { useForm } from 'react-hook-form' +import styles from '../../styles/ui.module.css' +import { corePluginHooks } from '../core/index' +import { imagePluginHooks } from './index' +import { DownshiftAutoComplete } from '../core/ui/DownshiftAutoComplete' + +interface ImageFormFields { + src: string + title: string + altText: string + file: FileList +} + +export const ImageDialog: React.FC = () => { + const [imageAutocompleteSuggestions, state] = imagePluginHooks.useEmitterValues('imageAutocompleteSuggestions', 'imageDialogState') + const saveImage = imagePluginHooks.usePublisher('saveImage') + const [editorRootElementRef] = corePluginHooks.useEmitterValues('editorRootElementRef') + const closeImageDialog = imagePluginHooks.usePublisher('closeImageDialog') + + const { register, handleSubmit, control, setValue, reset } = useForm({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + values: state.type === 'editing' ? (state.initialValues as any) : {} + }) + + return ( + { + if (!open) { + closeImageDialog(true) + reset({ src: '', title: '', altText: '' }) + } + }} + > + + + { + e.preventDefault() + }} + > +
{ + void handleSubmit(saveImage)(e) + reset({ src: '', title: '', altText: '' }) + e.nativeEvent.stopImmediatePropagation() + }} + className={styles.multiFieldForm} + > +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + +
+
+
+
+
+ ) +} diff --git a/src/plugins/image/ImageEditor.tsx b/src/plugins/image/ImageEditor.tsx index 9f8a14b3..914c2d85 100644 --- a/src/plugins/image/ImageEditor.tsx +++ b/src/plugins/image/ImageEditor.tsx @@ -5,6 +5,7 @@ import type { GridSelection, LexicalEditor, NodeSelection, RangeSelection } from import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection.js' import { mergeRegister } from '@lexical/utils' +import classNames from 'classnames' import { $getNodeByKey, $getSelection, @@ -19,11 +20,11 @@ import { KEY_ESCAPE_COMMAND, SELECTION_CHANGE_COMMAND } from 'lexical' +import { imagePluginHooks } from '.' +import SettingsIcon from '../../icons/settings.svg' import styles from '../../styles/ui.module.css' -import classNames from 'classnames' import { $isImageNode } from './ImageNode' import ImageResizer from './ImageResizer' -import { imagePluginHooks } from '.' export interface ImageEditorProps { nodeKey: string @@ -92,6 +93,7 @@ export function ImageEditor({ src, title, alt, nodeKey, width, height }: ImageEd const [disableImageResize] = imagePluginHooks.useEmitterValues('disableImageResize') const [imagePreviewHandler] = imagePluginHooks.useEmitterValues('imagePreviewHandler') const [imageSource, setImageSource] = React.useState(null) + const openEditImageDialog = imagePluginHooks.usePublisher('openEditImageDialog') const onDelete = React.useCallback( (payload: KeyboardEvent) => { @@ -256,6 +258,22 @@ export function ImageEditor({ src, title, alt, nodeKey, width, height }: ImageEd {draggable && isFocused && !disableImageResize && ( )} + ) : null diff --git a/src/plugins/image/ImageNode.tsx b/src/plugins/image/ImageNode.tsx index 7a86ac33..f0db0266 100644 --- a/src/plugins/image/ImageNode.tsx +++ b/src/plugins/image/ImageNode.tsx @@ -50,7 +50,7 @@ export class ImageNode extends DecoratorNode { __height: 'inherit' | number static getType(): string { - return 'image' + return 'icage' } static clone(node: ImageNode): ImageNode { @@ -170,6 +170,14 @@ export class ImageNode extends DecoratorNode { this.getWritable().__title = title } + setSrc(src: string): void { + this.getWritable().__src = src + } + + setAltText(altText: string | undefined): void { + this.getWritable().__altText = altText ?? '' + } + decorate(_parentEditor: LexicalEditor): JSX.Element { return ( } /** @internal */ @@ -46,28 +62,61 @@ export const imageSystem = system( const disableImageResize = r.node(false) const imageUploadHandler = r.node(null) const imagePreviewHandler = r.node(null) - - r.sub(r.pipe(insertImage, r.o.withLatestFrom(rootEditor, imageUploadHandler)), ([values, theEditor, imageUploadHandler]) => { - function insertImage(src: string) { - theEditor?.update(() => { - const imageNode = $createImageNode({ altText: values.altText ?? '', src, title: values.title ?? '' }) - $insertNodes([imageNode]) - if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) { - $wrapNodeInElement(imageNode, $createParagraphNode).selectEnd() - } - }) - } - - if (values.file) { - imageUploadHandler?.(values.file) - .then(insertImage) - .catch((e) => { - throw e - }) - } else if (values.src) { - insertImage(values.src) + const imageDialogState = r.node({ type: 'inactive' }) + const openNewImageDialog = r.node() + const openEditImageDialog = r.node>() + const closeImageDialog = r.node() + const saveImage = r.node() + + r.link(r.pipe(closeImageDialog, r.o.mapTo({ type: 'inactive' })), imageDialogState) + r.link(r.pipe(openNewImageDialog, r.o.mapTo({ type: 'new' })), imageDialogState) + + r.link( + r.pipe( + openEditImageDialog, + r.o.map((payload) => ({ type: 'editing', ...payload })) + ), + imageDialogState + ) + + r.sub( + r.pipe(saveImage, r.o.withLatestFrom(rootEditor, imageUploadHandler, imageDialogState)), + ([values, theEditor, imageUploadHandler, dialogState]) => { + const handler = + dialogState.type === 'editing' + ? (src: string) => { + theEditor?.update(() => { + const { nodeKey } = dialogState + const imageNode = $getNodeByKey(nodeKey) as ImageNode + + imageNode.setTitle(values.title) + imageNode.setAltText(values.altText) + imageNode.setSrc(src) + }) + r.pub(imageDialogState, { type: 'inactive' }) + } + : (src: string) => { + theEditor?.update(() => { + const imageNode = $createImageNode({ altText: values.altText ?? '', src, title: values.title ?? '' }) + $insertNodes([imageNode]) + if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) { + $wrapNodeInElement(imageNode, $createParagraphNode).selectEnd() + } + }) + r.pub(imageDialogState, { type: 'inactive' }) + } + + if (values.file.length > 0) { + imageUploadHandler?.(values.file.item(0)!) + .then(handler) + .catch((e) => { + throw e + }) + } else if (values.src) { + handler(values.src) + } } - }) + ) r.sub(rootEditor, (editor) => { editor?.registerCommand( @@ -128,7 +177,10 @@ export const imageSystem = system( Promise.all(cbPayload.map((file) => imageUploadHandlerValue(file.getAsFile()!))) .then((urls) => { urls.forEach((url) => { - r.pub(insertImage, url) + editor.dispatchCommand(INSERT_IMAGE_COMMAND, { + src: url, + altText: '' + }) }) }) .catch((e) => { @@ -141,6 +193,11 @@ export const imageSystem = system( }) return { + imageDialogState, + saveImage, + openNewImageDialog, + openEditImageDialog, + closeImageDialog, imageUploadHandler, imageAutocompleteSuggestions, disableImageResize, @@ -180,6 +237,7 @@ export const [ realm.pubKey('addImportVisitor', MdastJsxImageVisitor) realm.pubKey('addLexicalNode', ImageNode) realm.pubKey('addExportVisitor', LexicalImageVisitor) + realm.pubKey('addComposerChild', ImageDialog) } }) diff --git a/src/plugins/link-dialog/LinkDialog.tsx b/src/plugins/link-dialog/LinkDialog.tsx index 8b3b0e8b..e43f6db9 100644 --- a/src/plugins/link-dialog/LinkDialog.tsx +++ b/src/plugins/link-dialog/LinkDialog.tsx @@ -11,128 +11,76 @@ import CopyIcon from '../../icons/content_copy.svg' import EditIcon from '../../icons/edit.svg' import LinkOffIcon from '../../icons/link_off.svg' import OpenInNewIcon from '../../icons/open_in_new.svg' -import DropDownIcon from '../../icons/arrow_drop_down.svg' import classNames from 'classnames' -import { useCombobox } from 'downshift' import styles from '../../styles/ui.module.css' import { corePluginHooks } from '../core' import { linkDialogPluginHooks } from '.' +import { useForm } from 'react-hook-form' +import { DownshiftAutoComplete } from '../core/ui/DownshiftAutoComplete' export const OPEN_LINK_DIALOG: LexicalCommand = createCommand() interface LinkEditFormProps { - initialUrl: string - initialTitle: string - onSubmit: (link: [string, string]) => void + url: string + title: string + onSubmit: (link: { url: string; title: string }) => void onCancel: () => void linkAutocompleteSuggestions: string[] } -const MAX_SUGGESTIONS = 20 - -export function LinkEditForm({ initialUrl, initialTitle, onSubmit, onCancel, linkAutocompleteSuggestions }: LinkEditFormProps) { - const [items, setItems] = React.useState(linkAutocompleteSuggestions.slice(0, MAX_SUGGESTIONS)) - const [title, setTitle] = React.useState(initialTitle) - - const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, highlightedIndex, getItemProps, selectedItem, inputValue } = - useCombobox({ - initialInputValue: initialUrl, - onInputValueChange({ inputValue }) { - inputValue = inputValue?.toLowerCase() || '' - const matchingItems = [] - for (const url of linkAutocompleteSuggestions) { - if (url.toLowerCase().includes(inputValue)) { - matchingItems.push(url) - if (matchingItems.length >= MAX_SUGGESTIONS) { - break - } - } - } - setItems(matchingItems) - }, - items, - itemToString(item) { - return item ?? '' - } - }) - - const onSubmitEH = (e: React.FormEvent) => { - e.preventDefault() - onSubmit([inputValue, title]) - } - - const onKeyDownEH = React.useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - ;(e.target as HTMLInputElement).form?.reset() - } else if (e.key === 'Enter' && (!isOpen || items.length === 0)) { - e.preventDefault() - onSubmit([(e.target as HTMLInputElement).value, title]) - } - }, - [isOpen, items, onSubmit, title] - ) - - const handleSaveClick = (event: React.MouseEvent) => { - event.stopPropagation() - } - - const downshiftInputProps = getInputProps() +interface LinkFormFields { + url: string + title: string +} - const inputProps = { - ...downshiftInputProps, - onKeyDown: (e: React.KeyboardEvent) => { - onKeyDownEH(e) - downshiftInputProps.onKeyDown(e) +export function LinkEditForm({ url, title, onSubmit, onCancel, linkAutocompleteSuggestions }: LinkEditFormProps) { + const { + register, + handleSubmit, + control, + setValue, + reset: _ + } = useForm({ + values: { + url, + title } - } - - const dropdownIsVisible = isOpen && items.length > 0 + }) return ( -
-
+ { + void handleSubmit(onSubmit)(e) + e.nativeEvent.stopImmediatePropagation() + }} + onReset={onCancel} + className={classNames(styles.multiFieldForm, styles.linkDialogEditForm)} + > +
+
-
-
- - -
-
-
    - {items.map((item, index: number) => ( -
  • - {item} -
  • - ))} -
-
-
-
+
-
-
-
- setTitle(e.target.value)} /> -
+
+ -
) @@ -147,11 +95,10 @@ export const LinkDialog: React.FC = () => { 'linkDialogState', 'linkAutocompleteSuggestions' ) - const updateLinkUrl = linkDialogPluginHooks.usePublisher('updateLinkUrl') + const updateLink = linkDialogPluginHooks.usePublisher('updateLink') const cancelLinkEdit = linkDialogPluginHooks.usePublisher('cancelLinkEdit') const switchFromPreviewToLinkEdit = linkDialogPluginHooks.usePublisher('switchFromPreviewToLinkEdit') const removeLink = linkDialogPluginHooks.usePublisher('removeLink') - const applyLinkChanges = linkDialogPluginHooks.usePublisher('applyLinkChanges') React.useEffect(() => { const update = () => { @@ -173,14 +120,6 @@ export const LinkDialog: React.FC = () => { const theRect = linkDialogState?.rectangle - const onSubmitEH = React.useCallback( - (payload: [string, string]) => { - updateLinkUrl(payload) - applyLinkChanges(true) - }, - [applyLinkChanges, updateLinkUrl] - ) - const urlIsExternal = linkDialogState.type === 'preview' && linkDialogState.url.startsWith('http') return ( @@ -205,9 +144,9 @@ export const LinkDialog: React.FC = () => { > {linkDialogState.type === 'edit' && ( diff --git a/src/plugins/link-dialog/index.ts b/src/plugins/link-dialog/index.ts index 71a0b352..5674562b 100644 --- a/src/plugins/link-dialog/index.ts +++ b/src/plugins/link-dialog/index.ts @@ -64,7 +64,7 @@ const linkDialogSystem = system( const linkDialogState = r.node({ type: 'inactive' }, true) // actions - const updateLinkUrl = r.node<[string, string]>() + const updateLink = r.node<{ url: string; title: string }>() const cancelLinkEdit = r.node() const applyLinkChanges = r.node() const switchFromPreviewToLinkEdit = r.node() @@ -169,59 +169,36 @@ const linkDialogSystem = system( linkDialogState ) - r.sub(r.pipe(applyLinkChanges, r.o.withLatestFrom(linkDialogState, activeEditor)), ([, state, editor]) => { - if (state.type === 'edit') { - const url = state.url.trim() - const title = state.title.trim() + r.sub(r.pipe(updateLink, r.o.withLatestFrom(activeEditor, linkDialogState)), ([payload, editor, state]) => { + const url = payload.url.trim() + const title = payload.title.trim() - if (url.trim() !== '') { - editor?.dispatchCommand(TOGGLE_LINK_COMMAND, { url, title }) - // the dispatch command implementation fails to set the link for a fresh link creation. - // Work around with the code below. - setTimeout(() => { - editor?.update(() => { - const node = getLinkNodeInSelection($getSelection() as RangeSelection) - node?.setTitle(title) - }) - }) - r.pub(linkDialogState, { - type: 'preview', - linkNodeKey: state.linkNodeKey, - rectangle: state.rectangle, - url - }) - } else { - if (state.initialUrl !== '') { - editor?.dispatchCommand(TOGGLE_LINK_COMMAND, null) - } - r.pub(linkDialogState, { - type: 'inactive' + if (url.trim() !== '') { + editor?.dispatchCommand(TOGGLE_LINK_COMMAND, { url, title }) + // the dispatch command implementation fails to set the link for a fresh link creation. + // Work around with the code below. + setTimeout(() => { + editor?.update(() => { + const node = getLinkNodeInSelection($getSelection() as RangeSelection) + node?.setTitle(title) }) - } + }) + r.pub(linkDialogState, { + type: 'preview', + linkNodeKey: state.linkNodeKey, + rectangle: state.rectangle, + url + } as PreviewLinkDialog) } else { - throw new Error('Cannot apply link edit when not in edit mode') + if (state.type === 'edit' && state.initialUrl !== '') { + editor?.dispatchCommand(TOGGLE_LINK_COMMAND, null) + } + r.pub(linkDialogState, { + type: 'inactive' + }) } }) - r.link( - r.pipe( - updateLinkUrl, - r.o.withLatestFrom(linkDialogState), - r.o.map(([[url, title], state]) => { - if (state.type === 'edit') { - return { - ...state, - url, - title - } - } else { - throw new Error('Cannot update link url when not in edit mode') - } - }) - ), - linkDialogState - ) - r.link( r.pipe( cancelLinkEdit, @@ -283,7 +260,7 @@ const linkDialogSystem = system( dialogState, onWindowChange, linkDialogState, - updateLinkUrl, + updateLink, switchFromPreviewToLinkEdit, cancelLinkEdit, removeLink, diff --git a/src/plugins/toolbar/components/InsertImage.tsx b/src/plugins/toolbar/components/InsertImage.tsx index 9ae0d626..ee7314cc 100644 --- a/src/plugins/toolbar/components/InsertImage.tsx +++ b/src/plugins/toolbar/components/InsertImage.tsx @@ -1,178 +1,24 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import React from 'react' import { imagePluginHooks } from '../../image' import * as RadixToolbar from '@radix-ui/react-toolbar' -import * as Dialog from '@radix-ui/react-dialog' import AddPhotoIcon from '../../../icons/add_photo.svg' import styles from '../../../styles/ui.module.css' import { corePluginHooks } from '../../core/index' import { TooltipWrap } from '../primitives/TooltipWrap' -import { useCombobox } from 'downshift' -import classNames from 'classnames' -import DropDownIcon from '../../../icons/arrow_drop_down.svg' -import { useForm, Controller, Control } from 'react-hook-form' -interface ImageFormFields { - src?: string - title: string - altText?: string - file?: File -} - -const MAX_SUGGESTIONS = 20 /** * A toolbar button that allows the user to insert an image from an URL. * For the button to work, you need to have the `imagePlugin` plugin enabled. */ export const InsertImage = React.forwardRef>((_, forwardedRef) => { - const [imageAutocompleteSuggestions] = imagePluginHooks.useEmitterValues('imageAutocompleteSuggestions') - const insertImage = imagePluginHooks.usePublisher('insertImage') - const [editorRootElementRef, readOnly] = corePluginHooks.useEmitterValues('editorRootElementRef', 'readOnly') - const [open, setOpen] = React.useState(false) - const { register, handleSubmit, control } = useForm() + const openNewImageDialog = imagePluginHooks.usePublisher('openNewImageDialog') + const [readOnly] = corePluginHooks.useEmitterValues('readOnly') return ( - - - - - - - - - - - e.preventDefault()}> -
{ - insertImage(data) - setOpen(false) - })} - className={styles.multiFieldForm} - > -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - - - -
-
-
-
-
+ openNewImageDialog(true)}> + + + + ) }) - -const DownshiftAutoComplete: React.FC<{ suggestions: string[]; control: Control; placeholder: string; inputName: string }> = ({ - suggestions, - control, - inputName, - placeholder -}) => { - const [items, setItems] = React.useState(suggestions.slice(0, MAX_SUGGESTIONS)) - - const enableAutoComplete = suggestions.length > 0 - - const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, highlightedIndex, getItemProps, selectedItem } = useCombobox({ - initialInputValue: '', - onInputValueChange({ inputValue }) { - inputValue = inputValue?.toLowerCase() || '' - const matchingItems = [] - for (const suggestion of suggestions) { - if (suggestion.toLowerCase().includes(inputValue)) { - matchingItems.push(suggestion) - if (matchingItems.length >= MAX_SUGGESTIONS) { - break - } - } - } - setItems(matchingItems) - }, - items, - itemToString(item) { - return item ?? '' - } - }) - - const dropdownIsVisible = isOpen && items.length > 0 - return ( -
-
- { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const downshiftSrcProps = getInputProps({ - onSelect: field.onChange, - onBlur: field.onBlur, - ref: field.ref - }) - return ( - - ) - }} - /> - {enableAutoComplete && ( - - )} -
- -
-
    - {items.map((item, index: number) => ( -
  • - {item} -
  • - ))} -
-
-
- ) -} diff --git a/src/plugins/toolbar/primitives/DialogButton.tsx b/src/plugins/toolbar/primitives/DialogButton.tsx index c3a2f91d..d78adf66 100644 --- a/src/plugins/toolbar/primitives/DialogButton.tsx +++ b/src/plugins/toolbar/primitives/DialogButton.tsx @@ -147,7 +147,7 @@ const DialogForm: React.FC<{ const onSubmitEH = (e: React.FormEvent) => { e.preventDefault() - e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() onSubmitCallback((inputProps as { value: string }).value) } diff --git a/src/styles/ui.module.css b/src/styles/ui.module.css index ee49e1a4..5adfab5d 100644 --- a/src/styles/ui.module.css +++ b/src/styles/ui.module.css @@ -476,6 +476,7 @@ flex-direction: column; align-items: stretch; gap: var(--spacing-2); + padding: 0; } .linkDialogInputContainer { @@ -826,6 +827,18 @@ cursor: -webkit-grab; } +.editImageButton { + position: absolute; + right: var(--spacing-2); + top: var(--spacing-2); + background: var(--baseBase); + border-radius: var(--radius-full); + + & svg { + display: block; + } +} + .inlineEditor { display: inline-flex; border-radius: var(--radius-medium); @@ -1066,6 +1079,7 @@ all: unset; border-radius: var(--radius-base); border:1px solid var(--baseBorder); + background-color: var(--baseBase); padding: var(--spacing-2) var(--spacing-3); }