UNPKG

phx-react

Version:

PHX REACT

254 lines • 9.95 kB
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, } from 'lexical'; import { useEffect, useState } from 'react'; import * as React from 'react'; import { $createInlineImageNode, $isInlineImageNode, InlineImageNode, } from '../../nodes/InlineImageNode'; import { CAN_USE_DOM } from '../../shared/canUseDOM'; import { PHXModal } from '../../../Modal'; import { PHXUploadFile } from '../../../UploadFile'; import { PHXSelect } from '../../../Select'; import { PHXInput } from '../../../Input'; import { PHXCheckbox } from '../../../Checkbox'; /** * Gets the DOM selection for a target window when DOM APIs are available. * @param targetWindow Window to read selection from. * @returns DOM selection, or null when unavailable. */ const getDOMSelection = (targetWindow) => CAN_USE_DOM ? (targetWindow || window).getSelection() : null; export const INSERT_INLINE_IMAGE_COMMAND = createCommand('INSERT_INLINE_IMAGE_COMMAND'); /** * Renders the modal used to insert an inline image. * @param props Insert inline image modal props. * @returns Inline image insert modal element. */ export function InsertInlineImageModal({ activeEditor, setShow, show, }) { const [cdnLink, setCdnLink] = useState(''); let position = 'center'; let altText = 'photo'; let fileName = ''; let showCaption = false; /** * Hides the modal and resets local upload state. */ const onHide = () => { setShow(false); setCdnLink(''); fileName = ''; }; /** * Stores uploaded file metadata returned from the CDN uploader. * @param fileUpload Uploaded file object. * @param linkCdn CDN URLs returned by the uploader. */ const handleListenUpload = (fileUpload, linkCdn) => { fileName = fileUpload.name; setCdnLink(linkCdn[0]); }; /** * Dispatches the inline image insert command with the current modal values. */ const handleSubmit = () => { activeEditor.dispatchCommand(INSERT_INLINE_IMAGE_COMMAND, { altText, position, showCaption, src: cdnLink, }); onHide(); }; /** * Updates caption visibility from the checkbox field. * @param e Checkbox change event. */ const handleShowCaptionChange = (e) => { showCaption = e.target.checked; }; /** * Updates the selected inline image position. * @param e Select change event. */ const handlePositionChange = (e) => { position = e.target.value; }; return (React.createElement(PHXModal, { onHide: onHide, onPrimaryClick: handleSubmit, show: show, title: 'Insert Image' }, React.createElement("div", { className: 'space-y-4' }, React.createElement(PHXUploadFile, { apiCdnUpload: '/api/cdn-upload', basePath: '', defaultLink: [cdnLink], fileName: fileName, fileType: 'image', handleListenUpload: handleListenUpload, inCard: false, label: 'Image upload', moduleId: 'insert-editor-image', oneImg: true, projectId: 'phenikaa', titleHeaderAction: '' }), React.createElement(PHXInput, { defaultValue: altText, label: 'Alt Text', onChange: (e) => (altText = e.target.value) }), React.createElement(PHXSelect, { defaultValue: position, label: 'Position', onChange: handlePositionChange, style: { border: '1px gray solid', } }, React.createElement("option", { value: 'left' }, "Left"), React.createElement("option", { value: 'center' }, "Center"), React.createElement("option", { value: 'right' }, "Right"), React.createElement("option", { value: 'full' }, "Full Width")), React.createElement(PHXCheckbox, { key: 'show_cation', onChange: handleShowCaptionChange, title: 'Show Caption' })))); } /** * Registers inline image insertion and drag/drop behavior with Lexical. * @returns Null because the plugin renders no UI. */ export default function InlineImagePlugin() { const [editor] = useLexicalComposerContext(); useEffect(() => { if (!editor.hasNodes([InlineImageNode])) { throw new Error('ImagesPlugin: ImageNode not registered on editor'); } return mergeRegister(editor.registerCommand(INSERT_INLINE_IMAGE_COMMAND, (payload) => { const imageNode = $createInlineImageNode(payload); $insertNodes([imageNode]); if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) { $wrapNodeInElement(imageNode, $createParagraphNode).selectEnd(); } return true; }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(DRAGSTART_COMMAND, (event) => onDragStart(event), COMMAND_PRIORITY_HIGH), editor.registerCommand(DRAGOVER_COMMAND, (event) => onDragover(event), COMMAND_PRIORITY_LOW), editor.registerCommand(DROP_COMMAND, (event) => onDrop(event, editor), COMMAND_PRIORITY_HIGH)); }, [editor]); return null; } const TRANSPARENT_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; /** * Starts dragging the currently selected inline image. * @param event Drag start event from Lexical. * @returns True when drag data was written. */ function onDragStart(event) { const img = document.createElement('img'); img.src = TRANSPARENT_IMAGE; 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(), position: node.__position, showCaption: node.__showCaption, src: node.__src, width: node.__width, }, type: 'image', })); return true; } /** * Handles drag over behavior for selected inline images. * @param event Drag over event from Lexical. * @returns True when an inline image is being dragged. */ function onDragover(event) { const node = getImageNodeInSelection(); if (!node) { return false; } if (!canDropImage(event)) { event.preventDefault(); } return true; } /** * Drops the dragged inline image into a valid editor location. * @param event Drop event from Lexical. * @param editor Active Lexical editor. * @returns True when a selected inline image handled the drop. */ 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_INLINE_IMAGE_COMMAND, data); } return true; } /** * Gets the selected inline image node. * @returns Selected inline image node, or null when selection is not an inline image. */ function getImageNodeInSelection() { const selection = $getSelection(); if (!$isNodeSelection(selection)) { return null; } const nodes = selection.getNodes(); const node = nodes[0]; return $isInlineImageNode(node) ? node : null; } /** * Reads inline image payload from drag data. * @param event Drag event carrying data transfer payload. * @returns Inline image payload, or null when drag data is not an image payload. */ function getDragImageData(event) { var _a; const dragData = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData('application/x-lexical-drag'); if (!dragData) { return null; } const { data, type } = JSON.parse(dragData); if (type !== 'image') { return null; } return data; } /** * Checks whether an inline image can be dropped on the current event target. * @param event Drag event to inspect. * @returns True when the target is a valid editor drop location. */ function canDropImage(event) { const target = event.target; return !!(target && target instanceof HTMLElement && !target.closest('code, span.editor-image') && target.parentElement && target.parentElement.closest('div.ContentEditable__root')); } /** * Gets the DOM range at the drag event location. * @param event Drag event to inspect. * @returns DOM range for the drop location. */ 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; } //# sourceMappingURL=index.js.map