Spaces:
Running
on
Zero
Running
on
Zero
/** | |
* Copyright (c) Meta Platforms, Inc. and affiliates. | |
* | |
* This source code is licensed under the Chameleon License found in the | |
* LICENSE file in the root directory of this source tree. | |
* | |
*/ | |
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; | |
import { $wrapNodeInElement, mergeRegister } from "@lexical/utils"; | |
import { | |
$createParagraphNode, | |
$createRangeSelection, | |
$getSelection, | |
$insertNodes, | |
$isNodeSelection, | |
$isRootOrShadowRoot, | |
$setSelection, | |
COMMAND_PRIORITY_EDITOR, | |
COMMAND_PRIORITY_HIGH, | |
COMMAND_PRIORITY_LOW, | |
createCommand, | |
DRAGOVER_COMMAND, | |
DRAGSTART_COMMAND, | |
DROP_COMMAND, | |
LexicalCommand, | |
LexicalEditor, | |
} from "lexical"; | |
import { useEffect } from "react"; | |
import { | |
$createImageNode, | |
$isImageNode, | |
ImageNode, | |
ImagePayload, | |
} from "./ImageNode"; | |
export type InsertImagePayload = Readonly<ImagePayload>; | |
const getDOMSelection = (targetWindow: Window | null): Selection | null => | |
(targetWindow || window).getSelection(); | |
export const INSERT_IMAGE_COMMAND: LexicalCommand<InsertImagePayload> = | |
createCommand("INSERT_IMAGE_COMMAND"); | |
export function ImagesPlugin(): JSX.Element | null { | |
const [editor] = useLexicalComposerContext(); | |
useEffect(() => { | |
if (!editor.hasNodes([ImageNode])) { | |
throw new Error("ImagesPlugin: ImageNode not registered on editor"); | |
} | |
return mergeRegister( | |
editor.registerCommand<InsertImagePayload>( | |
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<DragEvent>( | |
DRAGSTART_COMMAND, | |
(event) => { | |
return onDragStart(event); | |
}, | |
COMMAND_PRIORITY_HIGH, | |
), | |
editor.registerCommand<DragEvent>( | |
DRAGOVER_COMMAND, | |
(event) => { | |
return onDragover(event); | |
}, | |
COMMAND_PRIORITY_LOW, | |
), | |
editor.registerCommand<DragEvent>( | |
DROP_COMMAND, | |
(event) => { | |
return onDrop(event, editor); | |
}, | |
COMMAND_PRIORITY_HIGH, | |
), | |
); | |
}, [editor]); | |
return null; | |
} | |
const TRANSPARENT_IMAGE = | |
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; | |
const img = document.createElement("img"); | |
img.src = TRANSPARENT_IMAGE; | |
function onDragStart(event: DragEvent): boolean { | |
const node = getImageNodeInSelection(); | |
if (!node) { | |
return false; | |
} | |
const dataTransfer = event.dataTransfer; | |
if (!dataTransfer) { | |
return false; | |
} | |
dataTransfer.setData("text/plain", "_"); | |
dataTransfer.setDragImage(img, 0, 0); | |
dataTransfer.setData( | |
"application/x-lexical-drag", | |
JSON.stringify({ | |
data: { | |
altText: node.__altText, | |
caption: node.__caption, | |
height: node.__height, | |
key: node.getKey(), | |
maxWidth: node.__maxWidth, | |
showCaption: node.__showCaption, | |
src: node.__src, | |
width: node.__width, | |
}, | |
type: "image", | |
}), | |
); | |
return true; | |
} | |
function onDragover(event: DragEvent): boolean { | |
const node = getImageNodeInSelection(); | |
if (!node) { | |
return false; | |
} | |
if (!canDropImage(event)) { | |
event.preventDefault(); | |
} | |
return true; | |
} | |
function onDrop(event: DragEvent, editor: LexicalEditor): boolean { | |
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(): ImageNode | null { | |
const selection = $getSelection(); | |
if (!$isNodeSelection(selection)) { | |
return null; | |
} | |
const nodes = selection.getNodes(); | |
const node = nodes[0]; | |
return $isImageNode(node) ? node : null; | |
} | |
function getDragImageData(event: DragEvent): null | InsertImagePayload { | |
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; | |
} | |
declare global { | |
interface DragEvent { | |
rangeOffset?: number; | |
rangeParent?: Node; | |
} | |
} | |
function canDropImage(event: DragEvent): boolean { | |
const target = event.target; | |
return !!( | |
target && | |
target instanceof HTMLElement && | |
!target.closest("code, span.editor-image") && | |
target.parentElement && | |
target.parentElement.closest("div.ContentEditable__root") | |
); | |
} | |
function getDragSelection(event: DragEvent): Range | null | undefined { | |
let range; | |
const target = event.target as null | Element | Document; | |
const targetWindow = | |
target == null | |
? null | |
: target.nodeType === 9 | |
? (target as Document).defaultView | |
: (target as Element).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; | |
} | |