130 lines
3.6 KiB
JavaScript
130 lines
3.6 KiB
JavaScript
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
|
import { $createTextNode, $getSelection, $insertNodes, $setSelection, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
|
|
import { $wrapNodeInElement, mergeRegister } from '@lexical/utils'
|
|
import { $createLinkNode, $isLinkNode } from '@lexical/link'
|
|
import { Modal } from 'react-bootstrap'
|
|
import React, { useState, useCallback, useContext, useRef, useEffect } from 'react'
|
|
import { Form, Input, SubmitButton } from '../../components/form'
|
|
import { ensureProtocol } from '../../lib/url'
|
|
import { getSelectedNode } from '../utils/selected-node'
|
|
import { namedUrlSchema } from '../../lib/validate'
|
|
|
|
export const INSERT_LINK_COMMAND = createCommand('INSERT_LINK_COMMAND')
|
|
|
|
export default function LinkInsertPlugin () {
|
|
const [editor] = useLexicalComposerContext()
|
|
|
|
useEffect(() => {
|
|
return mergeRegister(
|
|
editor.registerCommand(
|
|
INSERT_LINK_COMMAND,
|
|
(payload) => {
|
|
const selection = $getSelection()
|
|
const node = getSelectedNode(selection)
|
|
const parent = node.getParent()
|
|
if ($isLinkNode(parent)) {
|
|
parent.remove()
|
|
} else if ($isLinkNode(node)) {
|
|
node.remove()
|
|
}
|
|
const textNode = $createTextNode(payload.text)
|
|
$insertNodes([textNode])
|
|
const linkNode = $createLinkNode(payload.url)
|
|
$wrapNodeInElement(textNode, () => linkNode)
|
|
$setSelection(textNode.select())
|
|
return true
|
|
},
|
|
COMMAND_PRIORITY_EDITOR
|
|
)
|
|
)
|
|
}, [editor])
|
|
|
|
return null
|
|
}
|
|
|
|
export const LinkInsertContext = React.createContext({
|
|
link: null,
|
|
setLink: () => {}
|
|
})
|
|
|
|
export function LinkInsertProvider ({ children }) {
|
|
const [link, setLink] = useState(null)
|
|
|
|
const contextValue = {
|
|
link,
|
|
setLink: useCallback(link => setLink(link), [])
|
|
}
|
|
|
|
return (
|
|
<LinkInsertContext.Provider value={contextValue}>
|
|
<LinkInsertModal />
|
|
{children}
|
|
</LinkInsertContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useLinkInsert () {
|
|
const { link, setLink } = useContext(LinkInsertContext)
|
|
return { link, setLink }
|
|
}
|
|
|
|
export function LinkInsertModal () {
|
|
const [editor] = useLexicalComposerContext()
|
|
const { link, setLink } = useLinkInsert()
|
|
const inputRef = useRef(null)
|
|
|
|
useEffect(() => {
|
|
if (link) {
|
|
inputRef.current?.focus()
|
|
}
|
|
}, [link])
|
|
|
|
return (
|
|
<Modal
|
|
show={!!link}
|
|
onHide={() => {
|
|
setLink(null)
|
|
setTimeout(() => editor.focus(), 100)
|
|
}}
|
|
>
|
|
<div
|
|
className='modal-close' onClick={() => {
|
|
setLink(null)
|
|
// I think bootstrap messes with the focus on close so we have to do this ourselves
|
|
setTimeout(() => editor.focus(), 100)
|
|
}}
|
|
>X
|
|
</div>
|
|
<Modal.Body>
|
|
<Form
|
|
initial={{
|
|
text: link?.text,
|
|
url: link?.url
|
|
}}
|
|
schema={namedUrlSchema}
|
|
onSubmit={async ({ text, url }) => {
|
|
editor.dispatchCommand(INSERT_LINK_COMMAND, { url: ensureProtocol(url), text })
|
|
await setLink(null)
|
|
setTimeout(() => editor.focus(), 100)
|
|
}}
|
|
>
|
|
<Input
|
|
label='text'
|
|
name='text'
|
|
innerRef={inputRef}
|
|
required
|
|
/>
|
|
<Input
|
|
label='url'
|
|
name='url'
|
|
required
|
|
/>
|
|
<div className='d-flex'>
|
|
<SubmitButton variant='success' className='ml-auto mt-1 px-4'>ok</SubmitButton>
|
|
</div>
|
|
</Form>
|
|
</Modal.Body>
|
|
</Modal>
|
|
)
|
|
}
|