phx-react
Version:
PHX REACT
254 lines • 9.95 kB
JavaScript
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