diff --git a/packages/core/src/lib/hooks/useMediaInsert.ts b/packages/core/src/lib/hooks/useMediaInsert.ts index bb1c3d9f..8e755a2b 100644 --- a/packages/core/src/lib/hooks/useMediaInsert.ts +++ b/packages/core/src/lib/hooks/useMediaInsert.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { v4 as uuid } from 'uuid'; import { openMediaLibrary, removeInsertedMedia } from '@staticcms/core/actions/mediaLibrary'; @@ -39,17 +39,15 @@ export default function useMediaInsert controlID ?? uuid(), [controlID]); const mediaPathSelector = useMemo(() => selectMediaPath(finalControlID), [finalControlID]); const mediaPath = useAppSelector(mediaPathSelector); - const [selected, setSelected] = useState(false); useEffect(() => { - if (!selected && mediaPath && (mediaPath.path !== value.path || mediaPath.alt !== value.alt)) { - setSelected(true); + if (mediaPath && (mediaPath.path !== value.path || mediaPath.alt !== value.alt)) { setTimeout(() => { callback(mediaPath as MediaPath); dispatch(removeInsertedMedia(finalControlID)); }); } - }, [callback, finalControlID, dispatch, mediaPath, value, selected]); + }, [callback, finalControlID, dispatch, mediaPath, value]); const handleOpenMediaLibrary = useCallback( (e?: MouseEvent, { replaceIndex }: { replaceIndex?: number } = {}) => { @@ -69,7 +67,6 @@ export default function useMediaInsert = ({ left: `${selectionBoundingClientRect?.x}px`, }} /> - -
0 || debouncedGroups.length > 0 ? ( + - {groups.length > 0 ? groups : debouncedGroups} -
-
+
+ {groups.length > 0 ? groups : debouncedGroups} +
+ + ) : null} ); }; diff --git a/packages/core/src/widgets/markdown/plate/components/buttons/InsertImageToolbarButton.tsx b/packages/core/src/widgets/markdown/plate/components/buttons/InsertImageToolbarButton.tsx index d116a474..dbf0f95a 100644 --- a/packages/core/src/widgets/markdown/plate/components/buttons/InsertImageToolbarButton.tsx +++ b/packages/core/src/widgets/markdown/plate/components/buttons/InsertImageToolbarButton.tsx @@ -1,6 +1,6 @@ import { Image as ImageIcon } from '@styled-icons/material/Image'; -import { insertImage } from '@udecode/plate'; -import React, { useCallback, useMemo } from 'react'; +import { ELEMENT_IMAGE, getAboveNode, setNodes } from '@udecode/plate'; +import React, { useCallback, useMemo, useState } from 'react'; import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert'; import { isNotEmpty } from '@staticcms/core/lib/util/string.util'; @@ -8,7 +8,9 @@ import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes'; import ToolbarButton from './common/ToolbarButton'; import type { Collection, MarkdownField, MediaPath } from '@staticcms/core/interface'; +import type { MdImageElement } from '@staticcms/markdown/plate/plateTypes'; import type { FC } from 'react'; +import type { BaseSelection } from 'slate'; export interface InsertImageToolbarButtonProps { variant: 'button' | 'menu'; @@ -25,14 +27,42 @@ const InsertImageToolbarButton: FC = ({ currentValue, disabled, }) => { + const [selection, setSelection] = useState(); const editor = useMdPlateEditorState(); const handleInsert = useCallback( (newUrl: MediaPath) => { if (isNotEmpty(newUrl.path)) { - insertImage(editor, newUrl.path); + const image: MdImageElement = { + type: ELEMENT_IMAGE, + url: newUrl.path, + children: [{ text: '' }], + }; + + const imageAbove = getAboveNode(editor, { + at: selection?.focus, + match: { type: ELEMENT_IMAGE }, + }); + + if (imageAbove) { + if (newUrl.path !== imageAbove[0]?.url || newUrl.alt !== imageAbove[0]?.alt) { + setNodes( + editor, + { url: newUrl.path, alt: newUrl.alt }, + { + at: imageAbove[1], + }, + ); + } + + return; + } + + setNodes(editor, image, { + at: selection?.focus, + }); } }, - [editor], + [editor, selection], ); const chooseUrl = useMemo(() => field.choose_url ?? true, [field.choose_url]); @@ -46,12 +76,17 @@ const InsertImageToolbarButton: FC = ({ handleInsert, ); + const handleOpenMediaLibrary = useCallback(() => { + setSelection(editor.selection); + openMediaLibrary(); + }, [editor.selection, openMediaLibrary]); + return ( diff --git a/packages/core/src/widgets/markdown/plate/components/buttons/InsertLinkToolbarButton.tsx b/packages/core/src/widgets/markdown/plate/components/buttons/InsertLinkToolbarButton.tsx index 3138e777..269e1cb1 100644 --- a/packages/core/src/widgets/markdown/plate/components/buttons/InsertLinkToolbarButton.tsx +++ b/packages/core/src/widgets/markdown/plate/components/buttons/InsertLinkToolbarButton.tsx @@ -1,6 +1,15 @@ import { Link as LinkIcon } from '@styled-icons/material/Link'; -import { ELEMENT_LINK, getSelectionText, insertLink, someNode } from '@udecode/plate'; -import React, { useCallback, useMemo } from 'react'; +import { + ELEMENT_LINK, + getEditorString, + getNode, + getSelectionText, + insertLink, + replaceNodeChildren, + setNodes, + someNode, +} from '@udecode/plate'; +import React, { useCallback, useMemo, useState } from 'react'; import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert'; import useUUID from '@staticcms/core/lib/hooks/useUUID'; @@ -9,7 +18,10 @@ import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes'; import ToolbarButton from './common/ToolbarButton'; import type { Collection, MarkdownField, MediaPath } from '@staticcms/core/interface'; +import type { MdLinkElement } from '@staticcms/markdown/plate/plateTypes'; +import type { TText } from '@udecode/plate'; import type { FC } from 'react'; +import type { Path } from 'slate'; export interface InsertLinkToolbarButtonProps { variant: 'button' | 'menu'; @@ -26,18 +38,49 @@ const InsertLinkToolbarButton: FC = ({ currentValue, disabled, }) => { + const [selection, setSelection] = useState(); const editor = useMdPlateEditorState(); const handleInsert = useCallback( ({ path: newUrl, alt: newText }: MediaPath) => { - if (isNotEmpty(newUrl)) { + if (isNotEmpty(newUrl) && selection) { + const text = isNotEmpty(newText) ? newText : newUrl; + const linkAt = getNode(editor, selection); + + if (linkAt && linkAt.type === ELEMENT_LINK) { + if (newUrl !== linkAt.url || text !== linkAt.children[0].text) { + setNodes( + editor, + { url: newUrl, children: [{ text: newText }] }, + { at: selection }, + ); + + if (text !== getEditorString(editor, selection)) { + replaceNodeChildren(editor, { + at: selection, + nodes: { text }, + insertOptions: { + select: true, + }, + }); + } + } + + return; + } + insertLink( editor, - { url: newUrl, text: isNotEmpty(newText) ? newText : newUrl }, - { at: editor.selection ?? editor.prevSelection! }, + { url: newUrl, text }, + { + at: selection, + }, ); + const newSelection = [...selection]; + const lastIndex = newSelection.pop() ?? 0; + setSelection([...newSelection, lastIndex + 1]); } }, - [editor], + [editor, selection], ); const chooseUrl = useMemo(() => field.choose_url ?? true, [field.choose_url]); @@ -62,12 +105,17 @@ const InsertLinkToolbarButton: FC = ({ handleInsert, ); + const handleOpenMediaLibrary = useCallback(() => { + setSelection(editor.selection?.focus.path); + openMediaLibrary(); + }, [editor.selection, openMediaLibrary]); + return ( { - const { url, alt } = element; - const [internalValue, setInternalValue] = useState>({ path: url, alt }); + const { url, alt } = useMemo(() => element, [element]); const [popoverHasFocus, setPopoverHasFocus] = useState(false); const debouncedPopoverHasFocus = useDebounce(popoverHasFocus, 100); @@ -100,7 +99,6 @@ const withImageElement = ({ collection, entry, field }: WithImageElementProps) = (newValue: MediaPath) => { handleChange(newValue.path, 'url'); handleChange(newValue.alt ?? '', 'alt'); - setInternalValue(newValue); }, [handleChange], ); @@ -179,8 +177,8 @@ const withImageElement = ({ collection, entry, field }: WithImageElementProps) = anchorEl={anchorEl} collection={collection} field={field} - url={internalValue.path} - text={internalValue.alt} + url={url} + text={alt} onMediaChange={handleMediaChange} onRemove={handleRemove} forImage diff --git a/packages/core/src/widgets/markdown/plate/components/nodes/link/withLinkElement.tsx b/packages/core/src/widgets/markdown/plate/components/nodes/link/withLinkElement.tsx index 031856f1..5e906e53 100644 --- a/packages/core/src/widgets/markdown/plate/components/nodes/link/withLinkElement.tsx +++ b/packages/core/src/widgets/markdown/plate/components/nodes/link/withLinkElement.tsx @@ -8,7 +8,7 @@ import { unwrapLink, usePlateSelection, } from '@udecode/plate'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useFocused } from 'slate-react'; import useDebounce from '@staticcms/core/lib/hooks/useDebounce'; @@ -34,13 +34,10 @@ const withLinkElement = ({ collection, field }: WithLinkElementProps) => { }) => { const urlRef = useRef(null); - const { url } = element; const path = findNodePath(editor, element); - const [internalValue, setInternalValue] = useState>({ - path: url, - alt: getEditorString(editor, path), - }); + const { url } = useMemo(() => element, [element]); + const alt = useMemo(() => getEditorString(editor, path), [editor, path]); const [popoverHasFocus, setPopoverHasFocus] = useState(false); const debouncedPopoverHasFocus = useDebounce(popoverHasFocus, 100); @@ -114,7 +111,6 @@ const withLinkElement = ({ collection, field }: WithLinkElementProps) => { const handleMediaChange = useCallback( (newValue: MediaPath) => { handleChange(newValue.path, newValue.alt ?? ''); - setInternalValue(newValue); }, [handleChange], ); @@ -217,8 +213,8 @@ const withLinkElement = ({ collection, field }: WithLinkElementProps) => { anchorEl={anchorEl} collection={collection} field={field} - url={internalValue.path} - text={internalValue.alt} + url={url} + text={alt} onMediaChange={handleMediaChange} onRemove={handleRemove} onFocus={handlePopoverFocus}