Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(richtext-lexical): various UX improvements #6241

Merged
merged 7 commits into from
May 7, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import type { LinkFields } from '../nodes/types.js'
export interface Props {
drawerSlug: string
handleModalSubmit: (fields: FormState, data: Record<string, unknown>) => void
stateData?: LinkFields & { text: string }
stateData: {} | (LinkFields & { text: string })
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client'

import type { LexicalNode } from 'lexical'

import { $findMatchingParent } from '@lexical/utils'
import { $getSelection, $isRangeSelection } from 'lexical'

Expand Down Expand Up @@ -34,22 +36,35 @@ const toolbarGroups: ToolbarGroup[] = [
}
return false
},
isEnabled: ({ selection }) => {
return !!($isRangeSelection(selection) && $getSelection()?.getTextContent()?.length)
},
key: 'link',
label: `Link`,
onSelect: ({ editor, isActive }) => {
if (!isActive) {
let selectedText = null
let selectedText: string = null
let selectedNodes: LexicalNode[] = []
editor.getEditorState().read(() => {
selectedText = $getSelection().getTextContent()
selectedText = $getSelection()?.getTextContent()
// We need to selected nodes here before the drawer opens, as clicking around in the drawer may change the original selection
selectedNodes = $getSelection()?.getNodes() ?? []
})

if (!selectedText?.length) {
return
}

const linkFields: LinkFields = {
doc: null,
linkType: 'custom',
newTab: false,
url: 'https://',
}

editor.dispatchCommand(TOGGLE_LINK_WITH_MODAL_COMMAND, {
fields: linkFields,
selectedNodes,
text: selectedText,
})
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R

const { i18n, t } = useTranslation()

const [stateData, setStateData] = useState<LinkFields & { text: string }>(null)
const [stateData, setStateData] = useState<{} | (LinkFields & { text: string })>({})

const { closeModal, toggleModal } = useModal()
const editDepth = useEditDepth()
Expand All @@ -64,87 +64,100 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
depth: editDepth,
})

const setNotLink = useCallback(() => {
setIsLink(false)
if (editorRef && editorRef.current) {
editorRef.current.style.opacity = '0'
editorRef.current.style.transform = 'translate(-10000px, -10000px)'
}
setIsAutoLink(false)
setLinkUrl(null)
setLinkLabel(null)
setSelectedNodes([])
setStateData({})
}, [setIsLink, setLinkUrl, setLinkLabel, setSelectedNodes])

const updateLinkEditor = useCallback(() => {
const selection = $getSelection()
let selectedNodeDomRect: DOMRect | undefined = null

if (!$isRangeSelection(selection) || !selection) {
setNotLink()
return
}

// Handle the data displayed in the floating link editor & drawer when you click on a link node
if ($isRangeSelection(selection)) {
const focusNode = getSelectedNode(selection)
selectedNodeDomRect = editor.getElementByKey(focusNode.getKey())?.getBoundingClientRect()
const focusLinkParent: LinkNode = $findMatchingParent(focusNode, $isLinkNode)

// Prevent link modal from showing if selection spans further than the link: https://github.com/facebook/lexical/issues/4064
const badNode = selection
.getNodes()
.filter((node) => !$isLineBreakNode(node))
.find((node) => {
const linkNode = $findMatchingParent(node, $isLinkNode)
return (
(focusLinkParent && !focusLinkParent.is(linkNode)) ||
(linkNode && !linkNode.is(focusLinkParent))
)
})

if (focusLinkParent == null || badNode) {
setIsLink(false)
setIsAutoLink(false)
setLinkUrl(null)
setLinkLabel(null)
setSelectedNodes([])
return
}
const focusNode = getSelectedNode(selection)
selectedNodeDomRect = editor.getElementByKey(focusNode.getKey())?.getBoundingClientRect()
const focusLinkParent: LinkNode = $findMatchingParent(focusNode, $isLinkNode)

// Prevent link modal from showing if selection spans further than the link: https://github.com/facebook/lexical/issues/4064
const badNode = selection
.getNodes()
.filter((node) => !$isLineBreakNode(node))
.find((node) => {
const linkNode = $findMatchingParent(node, $isLinkNode)
return (
(focusLinkParent && !focusLinkParent.is(linkNode)) ||
(linkNode && !linkNode.is(focusLinkParent))
)
})

// Initial state:
const data: LinkFields & { text: string } = {
doc: undefined,
linkType: undefined,
newTab: undefined,
url: '',
...focusLinkParent.getFields(),
text: focusLinkParent.getTextContent(),
}
if (focusLinkParent == null || badNode) {
setNotLink()
return
}

if (focusLinkParent.getFields()?.linkType === 'custom') {
setLinkUrl(focusLinkParent.getFields()?.url ?? null)
setLinkLabel(null)
} else {
// internal link
setLinkUrl(
`/admin/collections/${focusLinkParent.getFields()?.doc?.relationTo}/${
focusLinkParent.getFields()?.doc?.value
}`,
)
// Initial state:
const data: LinkFields & { text: string } = {
doc: undefined,
linkType: undefined,
newTab: undefined,
url: '',
...focusLinkParent.getFields(),
text: focusLinkParent.getTextContent(),
}

const relatedField = config.collections.find(
(coll) => coll.slug === focusLinkParent.getFields()?.doc?.relationTo,
if (focusLinkParent.getFields()?.linkType === 'custom') {
setLinkUrl(focusLinkParent.getFields()?.url ?? null)
setLinkLabel(null)
} else {
// internal link
setLinkUrl(
`/admin/collections/${focusLinkParent.getFields()?.doc?.relationTo}/${
focusLinkParent.getFields()?.doc?.value
}`,
)

const relatedField = config.collections.find(
(coll) => coll.slug === focusLinkParent.getFields()?.doc?.relationTo,
)
if (!relatedField) {
// Usually happens if the user removed all default fields. In this case, we let them specify the label or do not display the label at all.
// label could be a virtual field the user added. This is useful if they want to use the link feature for things other than links.
setLinkLabel(
focusLinkParent.getFields()?.label ? String(focusLinkParent.getFields()?.label) : null,
)
setLinkUrl(
focusLinkParent.getFields()?.url ? String(focusLinkParent.getFields()?.url) : null,
)
if (!relatedField) {
// Usually happens if the user removed all default fields. In this case, we let them specify the label or do not display the label at all.
// label could be a virtual field the user added. This is useful if they want to use the link feature for things other than links.
setLinkLabel(
focusLinkParent.getFields()?.label ? String(focusLinkParent.getFields()?.label) : null,
)
setLinkUrl(
focusLinkParent.getFields()?.url ? String(focusLinkParent.getFields()?.url) : null,
)
} else {
const label = t('fields:linkedTo', {
label: getTranslation(relatedField.labels.singular, i18n),
}).replace(/<[^>]*>?/g, '')
setLinkLabel(label)
}
} else {
const label = t('fields:linkedTo', {
label: getTranslation(relatedField.labels.singular, i18n),
}).replace(/<[^>]*>?/g, '')
setLinkLabel(label)
}
}

setStateData(data)
setIsLink(true)
setSelectedNodes(selection ? selection?.getNodes() : [])
setStateData(data)
setIsLink(true)
setSelectedNodes(selection ? selection?.getNodes() : [])

if ($isAutoLinkNode(focusLinkParent)) {
setIsAutoLink(true)
} else {
setIsAutoLink(false)
}
if ($isAutoLinkNode(focusLinkParent)) {
setIsAutoLink(true)
} else {
setIsAutoLink(false)
}

const editorElem = editorRef.current
Expand All @@ -158,7 +171,6 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
const rootElement = editor.getRootElement()

if (
selection !== null &&
nativeSelection !== null &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
Expand All @@ -182,7 +194,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
}

return true
}, [anchorElem, editor, config, t, i18n])
}, [editor, setNotLink, config.collections, t, i18n, anchorElem])

useEffect(() => {
return mergeRegister(
Expand All @@ -202,13 +214,6 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
)
}, [editor, updateLinkEditor, toggleModal, drawerSlug])

useEffect(() => {
if (!isLink && editorRef) {
editorRef.current.style.opacity = '0'
editorRef.current.style.transform = 'translate(-10000px, -10000px)'
}
}, [isLink])

useEffect(() => {
const scrollerElem = anchorElem.parentElement

Expand Down Expand Up @@ -253,16 +258,16 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
KEY_ESCAPE_COMMAND,
() => {
if (isLink) {
setIsLink(false)
setIsAutoLink(false)
setNotLink()

return true
}
return false
},
COMMAND_PRIORITY_HIGH,
),
)
}, [editor, updateLinkEditor, setIsLink, isLink])
}, [editor, updateLinkEditor, isLink, setNotLink])

useEffect(() => {
editor.getEditorState().read(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ html[data-theme='light'] {
}

.link-editor {
z-index: 10;
z-index: 1;
display: flex;
align-items: center;
background: var(--color-base-0);
padding: 0px 3.72px 0px 6.25px;
padding: 0 3.72px 0 6.25px;
vertical-align: middle;
position: absolute;
top: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,10 @@ const Component: React.FC<Props> = (props) => {
if (event.shiftKey) {
setSelected(!isSelected)
} else {
clearSelection()
setSelected(true)
if (!isSelected) {
clearSelection()
setSelected(true)
}
}
return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ html[data-theme='dark'] {
.fixed-toolbar {
@include blur-bg(var(--theme-elevation-0));
display: flex;
align-items: center;
flex-wrap: wrap;
align-items: stretch;
padding: 0 3.72px 0 6.25px;
vertical-align: middle;
height: 37.5px;
position: sticky;
z-index: 2;
top: var(--doc-controls-height);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ html[data-theme='light'] {
position: absolute;
top: 0;
left: 0;
z-index: 1;
z-index: 2;
opacity: 0;
border-radius: 6.25px;
transition: opacity 0.2s;
Expand Down
Loading
Loading