text-editor-studio-ts
Version:
A powerful mobile-responsive rich text editor built with Lexical and React
337 lines (336 loc) • 9.84 kB
JavaScript
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
import { useRef, useState, useCallback, useEffect, Suspense } from "react";
import { u, o, a, c as $isImageNode, d as o$1, e as a$1, h, f as a$2, C as ContentEditable } from "./index-BwW17RmP.js";
import { l } from "./LexicalCollaborationContext.prod-_XIRbBSl.js";
import { f } from "./LexicalNestedComposer.prod-DcHyIDfI.js";
import { mergeRegister } from "@lexical/utils";
import { createCommand, $getSelection, $isNodeSelection, $setSelection, $isRangeSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW, CLICK_COMMAND, DRAGSTART_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, RootNode, TextNode, ParagraphNode, $getNodeByKey } from "lexical";
import { I as ImageResizer } from "./image-resizer-CFedlLqf.js";
const imageCache = /* @__PURE__ */ new Set();
const RIGHT_CLICK_IMAGE_COMMAND = createCommand("RIGHT_CLICK_IMAGE_COMMAND");
function useSuspenseImage(src) {
if (!imageCache.has(src)) {
throw new Promise((resolve) => {
const img = new Image();
img.src = src;
img.onload = () => {
imageCache.add(src);
resolve(null);
};
img.onerror = () => {
imageCache.add(src);
};
});
}
}
function LazyImage({
altText,
className,
imageRef,
src,
width,
height,
maxWidth,
onError
}) {
useSuspenseImage(src);
return /* @__PURE__ */ jsx(
"img",
{
className: className || void 0,
src,
alt: altText,
ref: imageRef,
style: {
height,
maxWidth,
width
},
onError,
draggable: "false"
}
);
}
function BrokenImage() {
return /* @__PURE__ */ jsx(
"img",
{
src: "",
style: {
height: 200,
opacity: 0.2,
width: 200
},
draggable: "false"
}
);
}
function ImageComponent({
src,
altText,
nodeKey,
width,
height,
maxWidth,
resizable,
showCaption,
caption,
captionsEnabled
}) {
const imageRef = useRef(null);
const buttonRef = useRef(null);
const [isSelected, setSelected, clearSelection] = u(nodeKey);
const [isResizing, setIsResizing] = useState(false);
const { isCollabActive } = l();
const [editor] = o();
const [selection, setSelection] = useState(null);
const activeEditorRef = useRef(null);
const [isLoadError, setIsLoadError] = useState(false);
const isEditable = a();
const $onDelete = useCallback(
(payload) => {
const deleteSelection = $getSelection();
if (isSelected && $isNodeSelection(deleteSelection)) {
const event = payload;
event.preventDefault();
editor.update(() => {
deleteSelection.getNodes().forEach((node) => {
if ($isImageNode(node)) {
node.remove();
}
});
});
}
return false;
},
[editor, isSelected]
);
const $onEnter = useCallback(
(event) => {
const latestSelection = $getSelection();
const buttonElem = buttonRef.current;
if (isSelected && $isNodeSelection(latestSelection) && latestSelection.getNodes().length === 1) {
if (showCaption) {
$setSelection(null);
event.preventDefault();
caption.focus();
return true;
} else if (buttonElem !== null && buttonElem !== document.activeElement) {
event.preventDefault();
buttonElem.focus();
return true;
}
}
return false;
},
[caption, isSelected, showCaption]
);
const $onEscape = useCallback(
(event) => {
if (activeEditorRef.current === caption || buttonRef.current === event.target) {
$setSelection(null);
editor.update(() => {
setSelected(true);
const parentRootElement = editor.getRootElement();
if (parentRootElement !== null) {
parentRootElement.focus();
}
});
return true;
}
return false;
},
[caption, editor, setSelected]
);
const onClick = useCallback(
(payload) => {
const event = payload;
if (isResizing) {
return true;
}
if (event.target === imageRef.current) {
if (event.shiftKey) {
setSelected(!isSelected);
} else {
clearSelection();
setSelected(true);
}
return true;
}
return false;
},
[isResizing, isSelected, setSelected, clearSelection]
);
const onRightClick = useCallback(
(event) => {
editor.getEditorState().read(() => {
const latestSelection = $getSelection();
const domElement = event.target;
if (domElement.tagName === "IMG" && $isRangeSelection(latestSelection) && latestSelection.getNodes().length === 1) {
editor.dispatchCommand(
RIGHT_CLICK_IMAGE_COMMAND,
event
);
}
});
},
[editor]
);
useEffect(() => {
let isMounted = true;
const rootElement = editor.getRootElement();
const unregister = mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
if (isMounted) {
setSelection(editorState.read(() => $getSelection()));
}
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_, activeEditor) => {
activeEditorRef.current = activeEditor;
return false;
},
COMMAND_PRIORITY_LOW
),
editor.registerCommand(
CLICK_COMMAND,
onClick,
COMMAND_PRIORITY_LOW
),
editor.registerCommand(
RIGHT_CLICK_IMAGE_COMMAND,
onClick,
COMMAND_PRIORITY_LOW
),
editor.registerCommand(
DRAGSTART_COMMAND,
(event) => {
if (event.target === imageRef.current) {
event.preventDefault();
return true;
}
return false;
},
COMMAND_PRIORITY_LOW
),
editor.registerCommand(
KEY_DELETE_COMMAND,
$onDelete,
COMMAND_PRIORITY_LOW
),
editor.registerCommand(
KEY_BACKSPACE_COMMAND,
$onDelete,
COMMAND_PRIORITY_LOW
),
editor.registerCommand(KEY_ENTER_COMMAND, $onEnter, COMMAND_PRIORITY_LOW),
editor.registerCommand(
KEY_ESCAPE_COMMAND,
$onEscape,
COMMAND_PRIORITY_LOW
)
);
rootElement?.addEventListener("contextmenu", onRightClick);
return () => {
isMounted = false;
unregister();
rootElement?.removeEventListener("contextmenu", onRightClick);
};
}, [
clearSelection,
editor,
isResizing,
isSelected,
nodeKey,
$onDelete,
$onEnter,
$onEscape,
onClick,
onRightClick,
setSelected
]);
const setShowCaption = () => {
editor.update(() => {
const node = $getNodeByKey(nodeKey);
if ($isImageNode(node)) {
node.setShowCaption(true);
}
});
};
const onResizeEnd = (nextWidth, nextHeight) => {
setTimeout(() => {
setIsResizing(false);
}, 200);
editor.update(() => {
const node = $getNodeByKey(nodeKey);
if ($isImageNode(node)) {
node.setWidthAndHeight(nextWidth, nextHeight);
}
});
};
const onResizeStart = () => {
setIsResizing(true);
};
const draggable = isSelected && $isNodeSelection(selection) && !isResizing;
const isFocused = (isSelected || isResizing) && isEditable;
return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx("div", { draggable, children: isLoadError ? /* @__PURE__ */ jsx(BrokenImage, {}) : /* @__PURE__ */ jsx(
LazyImage,
{
className: `max-w-full cursor-default ${isFocused ? `${$isNodeSelection(selection) ? "draggable cursor-grab active:cursor-grabbing" : ""} focused ring-2 ring-primary ring-offset-2` : null}`,
src,
altText,
imageRef,
width,
height,
maxWidth,
onError: () => setIsLoadError(true)
}
) }),
showCaption && /* @__PURE__ */ jsx("div", { className: "image-caption-container absolute bottom-1 left-0 right-0 m-0 block min-w-[100px] overflow-hidden border-t bg-background-system-body-primary/90 p-0", children: /* @__PURE__ */ jsxs(
f,
{
initialEditor: caption,
initialNodes: [RootNode, TextNode, ParagraphNode],
children: [
/* @__PURE__ */ jsx(o$1, {}),
/* @__PURE__ */ jsx(a$1, {}),
/* @__PURE__ */ jsx(
h,
{
contentEditable: /* @__PURE__ */ jsx(
ContentEditable,
{
className: "ImageNode__contentEditable user-select-text word-break-break-word relative block min-h-5 w-[calc(100%-20px)] cursor-text resize-none whitespace-pre-wrap border-0 p-2.5 text-sm caret-primary outline-none",
placeholderClassName: "ImageNode__placeholder text-sm text-content-system-global-secondary overflow-hidden absolute top-2.5 left-2.5 pointer-events-none text-ellipsis user-select-none whitespace-nowrap inline-block",
placeholder: "Enter a caption..."
}
),
ErrorBoundary: a$2
}
)
]
}
) }),
resizable && $isNodeSelection(selection) && isFocused && /* @__PURE__ */ jsx(
ImageResizer,
{
showCaption,
setShowCaption,
editor,
buttonRef,
imageRef,
maxWidth,
onResizeStart,
onResizeEnd,
captionsEnabled: !isLoadError && captionsEnabled
}
)
] }) });
}
export {
RIGHT_CLICK_IMAGE_COMMAND,
ImageComponent as default
};
//# sourceMappingURL=image-component-DFvqSqFs.js.map