text-editor-studio-ts
Version:
A powerful mobile-responsive rich text editor built with Lexical and React
335 lines (334 loc) • 10.7 kB
JavaScript
import { jsxs, Fragment, jsx } from "react/jsx-runtime";
import { useRef, useState, useCallback, useEffect, Suspense } from "react";
import { g as useEditorModal, u, o, a, i as $isInlineImageNode, B as Button, d as o$1, L as LinkPlugin, h, f as a$1, C as ContentEditable, j as Label, I as Input, S as Select, k as SelectTrigger, l as SelectValue, m as SelectContent, n as SelectItem, p as Checkbox, D as DialogFooter } from "./index-BwW17RmP.js";
import { f } from "./LexicalNestedComposer.prod-DcHyIDfI.js";
import { mergeRegister } from "@lexical/utils";
import { $getSelection, $isNodeSelection, $setSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW, CLICK_COMMAND, DRAGSTART_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, $getNodeByKey } from "lexical";
const imageCache = /* @__PURE__ */ new Set();
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);
};
});
}
}
function LazyImage({
altText,
className,
imageRef,
src,
width,
height,
position
}) {
useSuspenseImage(src);
return /* @__PURE__ */ jsx(
"img",
{
className: className || void 0,
src,
alt: altText,
ref: imageRef,
"data-position": position,
style: {
display: "block",
height,
width
},
draggable: "false"
}
);
}
function UpdateInlineImageDialog({
activeEditor,
nodeKey,
onClose
}) {
const editorState = activeEditor.getEditorState();
const node = editorState.read(
() => $getNodeByKey(nodeKey)
);
const [altText, setAltText] = useState(node.getAltText());
const [showCaption, setShowCaption] = useState(node.getShowCaption());
const [position, setPosition] = useState(node.getPosition());
const handleOnConfirm = () => {
const payload = { altText, position, showCaption };
if (node) {
activeEditor.update(() => {
node.update(payload);
});
}
onClose();
};
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsx(Label, { htmlFor: "alt-text", children: "Alt Text" }),
/* @__PURE__ */ jsx(
Input,
{
id: "alt-text",
placeholder: "Descriptive alternative text",
onChange: (e) => setAltText(e.target.value),
value: altText,
"data-test-id": "image-modal-alt-text-input"
}
)
] }),
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsx(Label, { htmlFor: "position-select", children: "Position" }),
/* @__PURE__ */ jsxs(
Select,
{
value: position,
onValueChange: (value) => setPosition(value),
children: [
/* @__PURE__ */ jsx(SelectTrigger, { id: "position-select", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select position" }) }),
/* @__PURE__ */ jsxs(SelectContent, { children: [
/* @__PURE__ */ jsx(SelectItem, { value: "left", children: "Left" }),
/* @__PURE__ */ jsx(SelectItem, { value: "right", children: "Right" }),
/* @__PURE__ */ jsx(SelectItem, { value: "full", children: "Full Width" })
] })
]
}
)
] }),
/* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2", children: [
/* @__PURE__ */ jsx(
Checkbox,
{
id: "caption",
checked: showCaption,
onCheckedChange: (checked) => setShowCaption(checked)
}
),
/* @__PURE__ */ jsx(Label, { htmlFor: "caption", children: "Show Caption" })
] }),
/* @__PURE__ */ jsx(DialogFooter, { children: /* @__PURE__ */ jsx(
Button,
{
"data-test-id": "image-modal-file-upload-btn",
onClick: handleOnConfirm,
children: "Confirm"
}
) })
] });
}
function InlineImageComponent({
src,
altText,
nodeKey,
width,
height,
showCaption,
caption,
position
}) {
const [modal, showModal] = useEditorModal();
const imageRef = useRef(null);
const buttonRef = useRef(null);
const [isSelected, setSelected, clearSelection] = u(nodeKey);
const [editor] = o();
const [selection, setSelection] = useState(null);
const activeEditorRef = useRef(null);
const isEditable = a();
const $onDelete = useCallback(
(payload) => {
const deleteSelection = $getSelection();
if (isSelected && $isNodeSelection(deleteSelection)) {
const event = payload;
event.preventDefault();
if (isSelected && $isNodeSelection(deleteSelection)) {
editor.update(() => {
deleteSelection.getNodes().forEach((node) => {
if ($isInlineImageNode(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]
);
useEffect(() => {
let isMounted = true;
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,
(payload) => {
const event = payload;
if (event.target === imageRef.current) {
if (event.shiftKey) {
setSelected(!isSelected);
} else {
clearSelection();
setSelected(true);
}
return true;
}
return false;
},
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
)
);
return () => {
isMounted = false;
unregister();
};
}, [
clearSelection,
editor,
isSelected,
nodeKey,
$onDelete,
$onEnter,
$onEscape,
setSelected
]);
const draggable = isSelected && $isNodeSelection(selection);
const isFocused = isSelected && isEditable;
return /* @__PURE__ */ jsxs(Suspense, { fallback: null, children: [
/* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs("span", { draggable, children: [
isEditable && /* @__PURE__ */ jsx(
Button,
{
className: "image-edit-button absolute right-1 top-1",
variant: "BorderStyle",
ref: buttonRef,
onClick: () => {
showModal("Update Inline Image", (onClose) => /* @__PURE__ */ jsx(
UpdateInlineImageDialog,
{
activeEditor: editor,
nodeKey,
onClose
}
));
},
children: "Edit"
}
),
/* @__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,
position
}
)
] }),
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, children: [
/* @__PURE__ */ jsx(o$1, {}),
/* @__PURE__ */ jsx(LinkPlugin, {}),
/* @__PURE__ */ jsx(
h,
{
contentEditable: /* @__PURE__ */ jsx(
ContentEditable,
{
placeholder: "Enter a caption...",
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"
}
),
ErrorBoundary: a$1
}
)
] }) })
] }),
modal
] });
}
export {
UpdateInlineImageDialog,
InlineImageComponent as default
};
//# sourceMappingURL=inline-image-component-D2wlU4Ob.js.map