stacker.news/lexical/plugins/image-insert.js

249 lines
6.0 KiB
JavaScript

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $wrapNodeInElement, mergeRegister } from '@lexical/utils'
import {
$createParagraphNode,
$createRangeSelection,
$getSelection,
$insertNodes,
$isRootOrShadowRoot,
$setSelection,
COMMAND_PRIORITY_EDITOR,
COMMAND_PRIORITY_HIGH,
COMMAND_PRIORITY_LOW,
createCommand,
DRAGOVER_COMMAND,
DRAGSTART_COMMAND,
DROP_COMMAND
} from 'lexical'
import { useEffect, useRef } from 'react'
import { ensureProtocol } from '../../lib/url'
import {
$createImageNode,
$isImageNode,
ImageNode
} from '../nodes/image'
import { Form, Input, SubmitButton } from '../../components/form'
import styles from '../styles.module.css'
import { urlSchema } from '../../lib/validate'
const getDOMSelection = (targetWindow) =>
typeof window !== 'undefined' ? (targetWindow || window).getSelection() : null
export const INSERT_IMAGE_COMMAND = createCommand('INSERT_IMAGE_COMMAND')
export function ImageInsertModal ({ onClose, editor }) {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return (
<Form
initial={{
url: '',
alt: ''
}}
schema={urlSchema}
onSubmit={async ({ alt, url }) => {
editor.dispatchCommand(INSERT_IMAGE_COMMAND, { src: ensureProtocol(url), altText: alt })
onClose()
}}
>
<Input
label='url'
name='url'
innerRef={inputRef}
required
autoFocus
/>
<Input
label={<>alt text <small className='text-muted ml-2'>optional</small></>}
name='alt'
/>
<div className='d-flex'>
<SubmitButton variant='success' className='ml-auto mt-1 px-4'>ok</SubmitButton>
</div>
</Form>
)
}
export default function ImageInsertPlugin ({
captionsEnabled
}) {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([ImageNode])) {
throw new Error('ImagesPlugin: ImageNode not registered on editor')
}
return mergeRegister(
editor.registerCommand(
INSERT_IMAGE_COMMAND,
(payload) => {
const imageNode = $createImageNode(payload)
$insertNodes([imageNode])
if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) {
$wrapNodeInElement(imageNode, $createParagraphNode).selectEnd()
}
return true
},
COMMAND_PRIORITY_EDITOR
),
editor.registerCommand(
DRAGSTART_COMMAND,
(event) => {
return onDragStart(event)
},
COMMAND_PRIORITY_HIGH
),
editor.registerCommand(
DRAGOVER_COMMAND,
(event) => {
return onDragover(event)
},
COMMAND_PRIORITY_LOW
),
editor.registerCommand(
DROP_COMMAND,
(event) => {
return onDrop(event, editor)
},
COMMAND_PRIORITY_HIGH
)
)
}, [captionsEnabled, editor])
return null
}
const TRANSPARENT_IMAGE =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
const img = typeof window !== 'undefined' ? document.createElement('img') : undefined
if (img) {
img.src = TRANSPARENT_IMAGE
}
function onDragStart (event) {
const node = getImageNodeInSelection()
if (!node) {
return false
}
const dataTransfer = event.dataTransfer
if (!dataTransfer) {
return false
}
dataTransfer.setData('text/plain', '_')
img.src = node.getSrc()
dataTransfer.setDragImage(img, 0, 0)
dataTransfer.setData(
'application/x-lexical-drag',
JSON.stringify({
data: {
altText: node.__altText,
caption: node.__caption,
height: node.__height,
maxHeight: '25vh',
key: node.getKey(),
maxWidth: node.__maxWidth,
showCaption: node.__showCaption,
src: node.__src,
width: node.__width
},
type: 'image'
})
)
return true
}
function onDragover (event) {
const node = getImageNodeInSelection()
if (!node) {
return false
}
if (!canDropImage(event)) {
event.preventDefault()
}
return true
}
function onDrop (event, editor) {
const node = getImageNodeInSelection()
if (!node) {
return false
}
const data = getDragImageData(event)
if (!data) {
return false
}
event.preventDefault()
if (canDropImage(event)) {
const range = getDragSelection(event)
node.remove()
const rangeSelection = $createRangeSelection()
if (range !== null && range !== undefined) {
rangeSelection.applyDOMRange(range)
}
$setSelection(rangeSelection)
editor.dispatchCommand(INSERT_IMAGE_COMMAND, data)
}
return true
}
function getImageNodeInSelection () {
const selection = $getSelection()
const nodes = selection.getNodes()
const node = nodes[0]
return $isImageNode(node) ? node : null
}
function getDragImageData (event) {
const dragData = event.dataTransfer?.getData('application/x-lexical-drag')
if (!dragData) {
return null
}
const { type, data } = JSON.parse(dragData)
if (type !== 'image') {
return null
}
return data
}
function canDropImage (event) {
const target = event.target
return !!(
target &&
target instanceof HTMLElement &&
!target.closest('code, span.editor-image') &&
target.parentElement &&
target.parentElement.closest(`div.${styles.editorInput}`)
)
}
function getDragSelection (event) {
let range
const target = event.target
const targetWindow =
target == null
? null
: target.nodeType === 9
? target.defaultView
: target.ownerDocument.defaultView
const domSelection = getDOMSelection(targetWindow)
if (document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(event.clientX, event.clientY)
} else if (event.rangeParent && domSelection !== null) {
domSelection.collapse(event.rangeParent, event.rangeOffset || 0)
range = domSelection.getRangeAt(0)
} else {
throw Error('Cannot get the selection when dragging')
}
return range
}