mirror of
https://github.com/standardnotes/app
synced 2026-01-16 19:04:58 -05:00
refactor: link editor [skip e2e]
This commit is contained in:
@@ -110,6 +110,7 @@
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.3.9",
|
||||
"@lexical/headless": "0.13.1",
|
||||
"@lexical/link": "0.13.1",
|
||||
"@lexical/list": "0.13.1",
|
||||
"@lexical/rich-text": "0.13.1",
|
||||
"@lexical/utils": "0.13.1",
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
LexicalEditor,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||
import { $isLinkTextNode } from './ToolbarLinkTextEditor'
|
||||
import { useElementResize } from '@/Hooks/useElementRect'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal'
|
||||
|
||||
const FloatingLinkEditor = ({
|
||||
linkUrl,
|
||||
linkText,
|
||||
isEditMode,
|
||||
setEditMode,
|
||||
editor,
|
||||
isAutoLink,
|
||||
isLinkText,
|
||||
isMobile,
|
||||
}: {
|
||||
linkUrl: string
|
||||
linkText: string
|
||||
isEditMode: boolean
|
||||
setEditMode: (isEditMode: boolean) => void
|
||||
editor: LexicalEditor
|
||||
isLinkText: boolean
|
||||
isAutoLink: boolean
|
||||
isMobile: boolean
|
||||
}) => {
|
||||
const [editedLinkUrl, setEditedLinkUrl] = useState(() => linkUrl)
|
||||
useEffect(() => {
|
||||
setEditedLinkUrl(linkUrl)
|
||||
}, [linkUrl])
|
||||
const [editedLinkText, setEditedLinkText] = useState(() => linkText)
|
||||
useEffect(() => {
|
||||
setEditedLinkText(linkText)
|
||||
}, [linkText])
|
||||
|
||||
const linkEditorRef = useRef<HTMLDivElement>(null)
|
||||
const rangeRect = useRef<DOMRect>()
|
||||
|
||||
const updateLinkEditorPosition = useCallback(() => {
|
||||
if (isMobile) {
|
||||
return
|
||||
}
|
||||
|
||||
const nativeSelection = window.getSelection()
|
||||
const rootElement = editor.getRootElement()
|
||||
|
||||
if (nativeSelection !== null && rootElement !== null) {
|
||||
if (rootElement.contains(nativeSelection.anchorNode)) {
|
||||
rangeRect.current = getDOMRangeRect(nativeSelection, rootElement)
|
||||
}
|
||||
}
|
||||
|
||||
const linkEditorElement = linkEditorRef.current
|
||||
|
||||
if (!linkEditorElement) {
|
||||
setTimeout(updateLinkEditorPosition)
|
||||
return
|
||||
}
|
||||
|
||||
if (!rootElement) {
|
||||
return
|
||||
}
|
||||
|
||||
const linkEditorRect = linkEditorElement.getBoundingClientRect()
|
||||
const rootElementRect = rootElement.getBoundingClientRect()
|
||||
|
||||
const calculatedStyles = getPositionedPopoverStyles({
|
||||
align: 'center',
|
||||
side: 'top',
|
||||
anchorRect: rangeRect.current,
|
||||
popoverRect: linkEditorRect,
|
||||
documentRect: rootElementRect,
|
||||
offset: 12,
|
||||
maxHeightFunction: () => 'none',
|
||||
})
|
||||
if (calculatedStyles) {
|
||||
const adjustedStyles = getAdjustedStylesForNonPortalPopover(linkEditorElement, calculatedStyles)
|
||||
Object.entries(adjustedStyles).forEach(([key, value]) => {
|
||||
linkEditorElement.style.setProperty(key, value)
|
||||
})
|
||||
linkEditorElement.style.opacity = '1'
|
||||
}
|
||||
}, [editor, isMobile])
|
||||
|
||||
useElementResize(linkEditorRef.current, updateLinkEditorPosition)
|
||||
|
||||
useEffect(() => {
|
||||
updateLinkEditorPosition()
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updateLinkEditorPosition()
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload) => {
|
||||
updateLinkEditorPosition()
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
)
|
||||
}, [editor, updateLinkEditorPosition])
|
||||
|
||||
const focusInput = useCallback((input: HTMLInputElement | null) => {
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSubmission = () => {
|
||||
if (editedLinkUrl !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl))
|
||||
}
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return
|
||||
}
|
||||
const node = getSelectedNode(selection)
|
||||
if (!$isLinkTextNode(node, selection)) {
|
||||
return
|
||||
}
|
||||
node.setTextContent(editedLinkText)
|
||||
})
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(updateLinkEditorPosition)
|
||||
}, [isEditMode, updateLinkEditorPosition])
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
id="super-link-editor"
|
||||
className="absolute bottom-12 left-1/2 z-modal w-[calc(100%_-_1rem)] -translate-x-1/2 rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)] md:bottom-[unset] md:left-0 md:top-0 md:w-auto md:translate-x-0 md:opacity-0"
|
||||
ref={linkEditorRef}
|
||||
>
|
||||
{isEditMode ? (
|
||||
<div
|
||||
className="flex flex-col gap-2 py-1"
|
||||
onBlur={(event) => {
|
||||
if (!linkEditorRef.current?.contains(event.relatedTarget as Node)) {
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isLinkText && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon type="plain-text" className="flex-shrink-0" />
|
||||
<input
|
||||
value={editedLinkText}
|
||||
onChange={(event) => {
|
||||
setEditedLinkText(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[20ch]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon type="link" className="flex-shrink-0" />
|
||||
<input
|
||||
ref={focusInput}
|
||||
value={editedLinkUrl}
|
||||
onChange={(event) => {
|
||||
setEditedLinkUrl(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[40ch]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditMode(false)
|
||||
editor.focus()
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Save link">
|
||||
<Button primary onClick={handleSubmission} onMouseDown={(event) => event.preventDefault()}>
|
||||
Apply
|
||||
</Button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
className={classNames(
|
||||
'mr-1 flex flex-grow items-center gap-2 overflow-hidden whitespace-nowrap underline',
|
||||
isAutoLink && 'py-2.5',
|
||||
)}
|
||||
href={linkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon type="open-in" className="ml-1 flex-shrink-0" />
|
||||
<div className="max-w-[35ch] overflow-hidden text-ellipsis">{linkUrl}</div>
|
||||
</a>
|
||||
<StyledTooltip showOnMobile showOnHover label="Copy link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(linkUrl).catch(console.error)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="copy" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
{!isAutoLink && (
|
||||
<>
|
||||
<StyledTooltip showOnMobile showOnHover label="Edit link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditedLinkUrl(linkUrl)
|
||||
setEditMode(true)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="pencil-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Remove link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="trash-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
document.getElementById(ElementIds.SuperEditor) ?? document.body,
|
||||
)
|
||||
}
|
||||
|
||||
export default FloatingLinkEditor
|
||||
@@ -0,0 +1,261 @@
|
||||
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
|
||||
import {
|
||||
$isTextNode,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
LexicalEditor,
|
||||
RangeSelection,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
TextNode,
|
||||
} from 'lexical'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||
import { useElementResize } from '@/Hooks/useElementRect'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal'
|
||||
|
||||
export const $isLinkTextNode = (
|
||||
node: ReturnType<typeof getSelectedNode>,
|
||||
selection: RangeSelection,
|
||||
): node is TextNode => {
|
||||
const parent = node.getParent()
|
||||
return (
|
||||
$isLinkNode(parent) &&
|
||||
parent.getChildrenSize() === 1 &&
|
||||
$isTextNode(node) &&
|
||||
parent.getFirstChild() === node &&
|
||||
selection.anchor.getNode() === selection.focus.getNode()
|
||||
)
|
||||
}
|
||||
|
||||
const LinkEditor = ({
|
||||
editor,
|
||||
setIsEditingLink,
|
||||
isMobile,
|
||||
linkNode,
|
||||
linkTextNode,
|
||||
}: {
|
||||
editor: LexicalEditor
|
||||
setIsEditingLink: (isEditMode: boolean) => void
|
||||
isMobile: boolean
|
||||
linkNode: LinkNode | null
|
||||
linkTextNode: TextNode | null
|
||||
}) => {
|
||||
const [url, setURL] = useState('')
|
||||
const [text, setText] = useState('')
|
||||
useEffect(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
if (linkNode) {
|
||||
setURL(linkNode.getURL())
|
||||
}
|
||||
if (linkTextNode) {
|
||||
setText(linkTextNode.getTextContent())
|
||||
}
|
||||
})
|
||||
}, [editor, linkNode, linkTextNode])
|
||||
|
||||
const linkInputRef = useRef<HTMLInputElement>(null)
|
||||
const linkEditorRef = useRef<HTMLDivElement>(null)
|
||||
const rangeRect = useRef<DOMRect>()
|
||||
const positionUpdateRAF = useRef<number>()
|
||||
|
||||
const updateLinkEditorPosition = useCallback(() => {
|
||||
if (positionUpdateRAF.current) {
|
||||
cancelAnimationFrame(positionUpdateRAF.current)
|
||||
}
|
||||
|
||||
positionUpdateRAF.current = requestAnimationFrame(() => {
|
||||
if (isMobile) {
|
||||
linkInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const nativeSelection = window.getSelection()
|
||||
const rootElement = editor.getRootElement()
|
||||
|
||||
if (nativeSelection !== null && rootElement !== null) {
|
||||
if (rootElement.contains(nativeSelection.anchorNode)) {
|
||||
rangeRect.current = getDOMRangeRect(nativeSelection, rootElement)
|
||||
}
|
||||
}
|
||||
|
||||
const linkEditorElement = linkEditorRef.current
|
||||
|
||||
if (!linkEditorElement) {
|
||||
setTimeout(updateLinkEditorPosition)
|
||||
return
|
||||
}
|
||||
|
||||
if (!rootElement) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!rangeRect.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const linkEditorRect = linkEditorElement.getBoundingClientRect()
|
||||
const rootElementRect = rootElement.getBoundingClientRect()
|
||||
|
||||
const calculatedStyles = getPositionedPopoverStyles({
|
||||
align: 'center',
|
||||
side: 'top',
|
||||
anchorRect: rangeRect.current,
|
||||
popoverRect: linkEditorRect,
|
||||
documentRect: rootElementRect,
|
||||
offset: 12,
|
||||
maxHeightFunction: () => 'none',
|
||||
})
|
||||
if (calculatedStyles) {
|
||||
const adjustedStyles = getAdjustedStylesForNonPortalPopover(linkEditorElement, calculatedStyles)
|
||||
Object.entries(adjustedStyles).forEach(([key, value]) => {
|
||||
linkEditorElement.style.setProperty(key, value)
|
||||
})
|
||||
linkEditorElement.style.display = 'block'
|
||||
linkInputRef.current?.focus()
|
||||
}
|
||||
})
|
||||
}, [editor, isMobile])
|
||||
|
||||
useElementResize(linkEditorRef.current, updateLinkEditorPosition)
|
||||
|
||||
useEffect(() => {
|
||||
updateLinkEditorPosition()
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updateLinkEditorPosition()
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload) => {
|
||||
updateLinkEditorPosition()
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
)
|
||||
}, [editor, updateLinkEditorPosition])
|
||||
|
||||
const handleSubmission = () => {
|
||||
if (url !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(url))
|
||||
}
|
||||
if (linkTextNode !== null && text !== '') {
|
||||
editor.update(
|
||||
() => {
|
||||
linkTextNode.setTextContent(text)
|
||||
},
|
||||
{
|
||||
discrete: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
setIsEditingLink(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const linkEditor = linkEditorRef.current
|
||||
if (!linkEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleFocusOut = (event: FocusEvent) => {
|
||||
if (!linkEditor.contains(event.relatedTarget as Node)) {
|
||||
setIsEditingLink(false)
|
||||
}
|
||||
}
|
||||
|
||||
linkEditor.addEventListener('focusout', handleFocusOut)
|
||||
|
||||
return () => {
|
||||
linkEditor.removeEventListener('focusout', handleFocusOut)
|
||||
}
|
||||
}, [setIsEditingLink])
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute z-dropdown-menu rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast',
|
||||
isMobile
|
||||
? 'bottom-12 left-1/2 w-[calc(100%_-_1rem)] -translate-x-1/2'
|
||||
: 'left-0 top-0 hidden w-auto translate-x-0 translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
|
||||
)}
|
||||
ref={linkEditorRef}
|
||||
>
|
||||
<div className="flex flex-col gap-2 py-1">
|
||||
{linkTextNode && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon type="plain-text" className="flex-shrink-0" />
|
||||
<input
|
||||
value={text}
|
||||
onChange={(event) => {
|
||||
setText(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setIsEditingLink(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm border border-border bg-contrast p-1 text-text sm:min-w-[20ch] translucent-ui:md:border-0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon type="link" className="flex-shrink-0" />
|
||||
<input
|
||||
ref={linkInputRef}
|
||||
value={url}
|
||||
onChange={(event) => {
|
||||
setURL(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setIsEditingLink(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm border border-border bg-contrast p-1 text-text sm:min-w-[40ch] translucent-ui:md:border-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsEditingLink(false)
|
||||
editor.focus()
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Save link">
|
||||
<Button primary onClick={handleSubmission} onMouseDown={(event) => event.preventDefault()}>
|
||||
Apply
|
||||
</Button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.getElementById(ElementIds.SuperEditor) ?? document.body,
|
||||
)
|
||||
}
|
||||
|
||||
export default LinkEditor
|
||||
@@ -0,0 +1,172 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
|
||||
import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import { useElementResize } from '@/Hooks/useElementRect'
|
||||
import { $isAutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { COMMAND_PRIORITY_LOW, LexicalEditor, SELECTION_CHANGE_COMMAND } from 'lexical'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
type Props = {
|
||||
linkNode: LinkNode
|
||||
editor: LexicalEditor
|
||||
isMobile: boolean
|
||||
setIsEditingLink: (isEditingLink: boolean) => void
|
||||
}
|
||||
|
||||
const LinkViewer = ({ isMobile, editor, linkNode, setIsEditingLink }: Props) => {
|
||||
const linkViewerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [linkUrl, isAutoLink] = useMemo(() => {
|
||||
let linkUrl = ''
|
||||
let isAutoLink = false
|
||||
editor.getEditorState().read(() => {
|
||||
linkUrl = linkNode.getURL()
|
||||
isAutoLink = $isAutoLinkNode(linkNode)
|
||||
})
|
||||
return [linkUrl, isAutoLink]
|
||||
}, [editor, linkNode])
|
||||
|
||||
const rangeRect = useRef<DOMRect>()
|
||||
const updateLinkEditorPosition = useCallback(() => {
|
||||
if (isMobile) {
|
||||
return
|
||||
}
|
||||
|
||||
const nativeSelection = window.getSelection()
|
||||
const rootElement = editor.getRootElement()
|
||||
|
||||
if (nativeSelection !== null && rootElement !== null) {
|
||||
if (rootElement.contains(nativeSelection.anchorNode)) {
|
||||
rangeRect.current = getDOMRangeRect(nativeSelection, rootElement)
|
||||
}
|
||||
}
|
||||
|
||||
const linkEditorElement = linkViewerRef.current
|
||||
|
||||
if (!linkEditorElement) {
|
||||
setTimeout(updateLinkEditorPosition)
|
||||
return
|
||||
}
|
||||
|
||||
if (!rootElement) {
|
||||
return
|
||||
}
|
||||
|
||||
const linkEditorRect = linkEditorElement.getBoundingClientRect()
|
||||
const rootElementRect = rootElement.getBoundingClientRect()
|
||||
|
||||
const calculatedStyles = getPositionedPopoverStyles({
|
||||
align: 'center',
|
||||
side: 'top',
|
||||
anchorRect: rangeRect.current,
|
||||
popoverRect: linkEditorRect,
|
||||
documentRect: rootElementRect,
|
||||
offset: 12,
|
||||
maxHeightFunction: () => 'none',
|
||||
})
|
||||
if (calculatedStyles) {
|
||||
const adjustedStyles = getAdjustedStylesForNonPortalPopover(linkEditorElement, calculatedStyles)
|
||||
Object.entries(adjustedStyles).forEach(([key, value]) => {
|
||||
linkEditorElement.style.setProperty(key, value)
|
||||
})
|
||||
linkEditorElement.style.opacity = '1'
|
||||
}
|
||||
}, [editor, isMobile])
|
||||
|
||||
useElementResize(linkViewerRef.current, updateLinkEditorPosition)
|
||||
|
||||
useEffect(() => {
|
||||
updateLinkEditorPosition()
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updateLinkEditorPosition()
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload) => {
|
||||
updateLinkEditorPosition()
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
)
|
||||
}, [editor, updateLinkEditorPosition])
|
||||
|
||||
if (!linkUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute z-dropdown-menu rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast',
|
||||
isMobile
|
||||
? 'bottom-12 left-1/2 w-[calc(100%_-_1rem)] -translate-x-1/2'
|
||||
: 'left-0 top-0 w-auto translate-x-0 opacity-0 translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
|
||||
)}
|
||||
ref={linkViewerRef}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
className={classNames(
|
||||
'mr-1 flex flex-grow items-center gap-2 overflow-hidden whitespace-nowrap underline',
|
||||
isAutoLink && 'py-2.5',
|
||||
)}
|
||||
href={linkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon type="open-in" className="ml-1 flex-shrink-0" />
|
||||
<div className="max-w-[35ch] overflow-hidden text-ellipsis">{linkUrl}</div>
|
||||
</a>
|
||||
<StyledTooltip showOnMobile showOnHover label="Copy link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(linkUrl).catch(console.error)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="copy" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
{!isAutoLink && (
|
||||
<>
|
||||
<StyledTooltip showOnMobile showOnHover label="Edit link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setIsEditingLink(true)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="pencil-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Remove link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="trash-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.getElementById(ElementIds.SuperEditor) ?? document.body,
|
||||
)
|
||||
}
|
||||
|
||||
export default LinkViewer
|
||||
@@ -1,131 +0,0 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { useCallback, useState, useRef, useEffect } from 'react'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
|
||||
type Props = {
|
||||
linkUrl: string
|
||||
isEditMode: boolean
|
||||
setEditMode: (isEditMode: boolean) => void
|
||||
editor: LexicalEditor
|
||||
isAutoLink: boolean
|
||||
}
|
||||
|
||||
const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, isAutoLink }: Props) => {
|
||||
const [editedLinkUrl, setEditedLinkUrl] = useState('')
|
||||
const editModeContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleLinkSubmission = () => {
|
||||
if (editedLinkUrl !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl))
|
||||
}
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
const focusInput = useCallback((input: HTMLInputElement | null) => {
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setEditedLinkUrl(linkUrl)
|
||||
}, [linkUrl])
|
||||
|
||||
return isEditMode ? (
|
||||
<div className="flex items-center gap-2" ref={editModeContainer}>
|
||||
<input
|
||||
id="link-input"
|
||||
ref={focusInput}
|
||||
value={editedLinkUrl}
|
||||
onChange={(event) => {
|
||||
setEditedLinkUrl(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleLinkSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (!editModeContainer.current?.contains(event.relatedTarget as Node)) {
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[40ch]"
|
||||
/>
|
||||
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditMode(false)
|
||||
editor.focus()
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="close" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Save link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={handleLinkSubmission}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="check" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
className={classNames(
|
||||
'mr-1 flex flex-grow items-center gap-2 overflow-hidden whitespace-nowrap underline',
|
||||
isAutoLink && 'py-2.5',
|
||||
)}
|
||||
href={linkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon type="open-in" className="ml-1 flex-shrink-0" />
|
||||
<div className="max-w-[35ch] overflow-hidden text-ellipsis">{linkUrl}</div>
|
||||
</a>
|
||||
{!isAutoLink && (
|
||||
<>
|
||||
<StyledTooltip showOnMobile showOnHover label="Edit link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditedLinkUrl(linkUrl)
|
||||
setEditMode(true)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="pencil-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Remove link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="trash-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinkEditor
|
||||
@@ -1,124 +0,0 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { $getSelection, $isRangeSelection, $isTextNode, LexicalEditor, RangeSelection, TextNode } from 'lexical'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { VisuallyHidden } from '@ariakit/react'
|
||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||
import { $isLinkNode } from '@lexical/link'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
|
||||
type Props = {
|
||||
linkText: string
|
||||
editor: LexicalEditor
|
||||
isEditMode: boolean
|
||||
setEditMode: (isEditMode: boolean) => void
|
||||
}
|
||||
|
||||
export const $isLinkTextNode = (
|
||||
node: ReturnType<typeof getSelectedNode>,
|
||||
selection: RangeSelection,
|
||||
): node is TextNode => {
|
||||
const parent = node.getParent()
|
||||
return $isLinkNode(parent) && $isTextNode(node) && selection.anchor.getNode() === selection.focus.getNode()
|
||||
}
|
||||
|
||||
const LinkTextEditor = ({ linkText, editor, isEditMode, setEditMode }: Props) => {
|
||||
const [editedLinkText, setEditedLinkText] = useState(() => linkText)
|
||||
const editModeContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setEditedLinkText(linkText)
|
||||
}, [linkText])
|
||||
|
||||
const focusInput = useCallback((input: HTMLInputElement | null) => {
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLinkTextSubmission = () => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return
|
||||
}
|
||||
const node = getSelectedNode(selection)
|
||||
if (!$isLinkTextNode(node, selection)) {
|
||||
return
|
||||
}
|
||||
node.setTextContent(editedLinkText)
|
||||
})
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
return isEditMode ? (
|
||||
<div className="flex items-center gap-2" ref={editModeContainer}>
|
||||
<input
|
||||
id="link-input"
|
||||
ref={focusInput}
|
||||
value={editedLinkText}
|
||||
onChange={(event) => {
|
||||
setEditedLinkText(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleLinkTextSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (!editModeContainer.current?.contains(event.relatedTarget as Node)) {
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[20ch]"
|
||||
/>
|
||||
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditMode(false)
|
||||
editor.focus()
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="close" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Save link text">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={handleLinkTextSubmission}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="check" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon type="plain-text" className="ml-1 mr-1 flex-shrink-0" />
|
||||
<div className="flex-grow overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<VisuallyHidden>Link text:</VisuallyHidden>
|
||||
{linkText}
|
||||
</div>
|
||||
<StyledTooltip showOnMobile showOnHover label="Edit link text">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditedLinkText(linkText)
|
||||
setEditMode(true)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="pencil-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinkTextEditor
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
$createParagraphNode,
|
||||
$isTextNode,
|
||||
$getNodeByKey,
|
||||
TextNode,
|
||||
} from 'lexical'
|
||||
import {
|
||||
mergeRegister,
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
$getNearestNodeOfType,
|
||||
$getNearestBlockElementAncestorOrThrow,
|
||||
} from '@lexical/utils'
|
||||
import { $isLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND, LinkNode } from '@lexical/link'
|
||||
import { $isListNode, ListNode } from '@lexical/list'
|
||||
import { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text'
|
||||
import {
|
||||
@@ -61,13 +62,12 @@ import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import { Toolbar, ToolbarItem, useToolbarStore } from '@ariakit/react'
|
||||
import { PasswordBlock } from '../Blocks/Password'
|
||||
import { URL_REGEX } from '@/Constants/Constants'
|
||||
import { $isLinkTextNode } from './ToolbarLinkTextEditor'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem, { MenuItemProps } from '@/Components/Menu/MenuItem'
|
||||
import { debounce, remToPx } from '@/Utils'
|
||||
import FloatingLinkEditor from './FloatingLinkEditor'
|
||||
import LinkEditor, { $isLinkTextNode } from './LinkEditor'
|
||||
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { useStateRef } from '@/Hooks/useStateRef'
|
||||
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
|
||||
@@ -75,6 +75,7 @@ import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPo
|
||||
import usePreference from '@/Hooks/usePreference'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import LinkViewer from './LinkViewer'
|
||||
|
||||
const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND')
|
||||
|
||||
@@ -220,12 +221,9 @@ const ToolbarPlugin = () => {
|
||||
const [isCode, setIsCode] = useState(false)
|
||||
const [isHighlight, setIsHighlight] = useState(false)
|
||||
|
||||
const [isLink, setIsLink] = useState(false)
|
||||
const [isAutoLink, setIsAutoLink] = useState(false)
|
||||
const [isLinkText, setIsLinkText] = useState(false)
|
||||
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
|
||||
const [linkText, setLinkText] = useState<string>('')
|
||||
const [linkUrl, setLinkUrl] = useState<string>('')
|
||||
const [linkNode, setLinkNode] = useState<LinkNode | null>(null)
|
||||
const [linkTextNode, setLinkTextNode] = useState<TextNode | null>(null)
|
||||
const [isEditingLink, setIsEditingLink] = useState(false)
|
||||
|
||||
const [isTOCOpen, setIsTOCOpen] = useState(false)
|
||||
const tocAnchorRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -342,23 +340,18 @@ const ToolbarPlugin = () => {
|
||||
// Update links
|
||||
const node = getSelectedNode(selection)
|
||||
const parent = node.getParent()
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
setIsLink(true)
|
||||
setIsEditingLink(false)
|
||||
if ($isLinkNode(node)) {
|
||||
setLinkNode(node)
|
||||
} else if ($isLinkNode(parent)) {
|
||||
setLinkNode(parent)
|
||||
} else {
|
||||
setIsLink(false)
|
||||
}
|
||||
setLinkUrl($isLinkNode(parent) ? parent.getURL() : $isLinkNode(node) ? node.getURL() : '')
|
||||
if ($isAutoLinkNode(parent) || $isAutoLinkNode(node)) {
|
||||
setIsAutoLink(true)
|
||||
} else {
|
||||
setIsAutoLink(false)
|
||||
setLinkNode(null)
|
||||
}
|
||||
if ($isLinkTextNode(node, selection)) {
|
||||
setIsLinkText(true)
|
||||
setLinkText(node.getTextContent())
|
||||
setLinkTextNode(node)
|
||||
} else {
|
||||
setIsLinkText(false)
|
||||
setLinkText('')
|
||||
setLinkTextNode(null)
|
||||
}
|
||||
|
||||
if (elementDOM !== null) {
|
||||
@@ -505,13 +498,11 @@ const ToolbarPlugin = () => {
|
||||
TOGGLE_LINK_AND_EDIT_COMMAND,
|
||||
(payload) => {
|
||||
if (payload === null) {
|
||||
setIsEditingLink(false)
|
||||
return activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
} else if (typeof payload === 'string') {
|
||||
const dispatched = activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, payload)
|
||||
setIsLink(true)
|
||||
setLinkUrl(payload)
|
||||
setIsLinkEditMode(true)
|
||||
return dispatched
|
||||
} else {
|
||||
setIsEditingLink(true)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
@@ -542,11 +533,9 @@ const ToolbarPlugin = () => {
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
activeEditor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
setIsLinkEditMode(true)
|
||||
})
|
||||
} else {
|
||||
activeEditor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
setIsLinkEditMode(true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -555,7 +544,7 @@ const ToolbarPlugin = () => {
|
||||
},
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
)
|
||||
}, [activeEditor, isLink])
|
||||
}, [activeEditor])
|
||||
|
||||
const dismissButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
@@ -580,7 +569,7 @@ const ToolbarPlugin = () => {
|
||||
const elementToBeFocused = event.relatedTarget as Node
|
||||
const containerContainsElementToFocus = container?.contains(elementToBeFocused)
|
||||
const linkEditorContainsElementToFocus = document
|
||||
.getElementById('super-link-editor')
|
||||
.getElementById(ElementIds.SuperEditor)
|
||||
?.contains(elementToBeFocused)
|
||||
const willFocusDismissButton = dismissButtonRef.current === elementToBeFocused
|
||||
if ((containerContainsElementToFocus || linkEditorContainsElementToFocus) && !willFocusDismissButton) {
|
||||
@@ -664,16 +653,22 @@ const ToolbarPlugin = () => {
|
||||
id="super-mobile-toolbar"
|
||||
ref={containerRef}
|
||||
>
|
||||
{isLink && (
|
||||
<FloatingLinkEditor
|
||||
linkUrl={linkUrl}
|
||||
linkText={linkText}
|
||||
isEditMode={isLinkEditMode}
|
||||
setEditMode={setIsLinkEditMode}
|
||||
editor={editor}
|
||||
isAutoLink={isAutoLink}
|
||||
isLinkText={isLinkText}
|
||||
{linkNode && !isEditingLink && (
|
||||
<LinkViewer
|
||||
key={linkNode.__key}
|
||||
linkNode={linkNode}
|
||||
isMobile={isMobile}
|
||||
setIsEditingLink={setIsEditingLink}
|
||||
editor={activeEditor}
|
||||
/>
|
||||
)}
|
||||
{isEditingLink && (
|
||||
<LinkEditor
|
||||
editor={activeEditor}
|
||||
setIsEditingLink={setIsEditingLink}
|
||||
isMobile={isMobile}
|
||||
linkNode={linkNode}
|
||||
linkTextNode={linkTextNode}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full flex-shrink-0 border-t border-border md:border-0">
|
||||
@@ -742,7 +737,7 @@ const ToolbarPlugin = () => {
|
||||
<ToolbarButton
|
||||
name="Link"
|
||||
iconName="link"
|
||||
active={isLink}
|
||||
active={!!linkNode}
|
||||
onSelect={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user