@mdxeditor/editor
Version:
React component for rich text markdown editing
298 lines (297 loc) • 10.6 kB
JavaScript
import { $isLinkNode, $createLinkNode, TOGGLE_LINK_COMMAND, LinkNode } from "@lexical/link";
import { Signal, Cell, Action, withLatestFrom, map, filter } from "@mdxeditor/gurx";
import { KEY_ESCAPE_COMMAND, COMMAND_PRIORITY_LOW, KEY_MODIFIER_COMMAND, $getSelection, $isRangeSelection, COMMAND_PRIORITY_HIGH, $getNodeByKey, $createTextNode, $insertNodes, $isTextNode, $getNearestNodeFromDOMNode } from "lexical";
import { realmPlugin } from "../../RealmWithPlugins.js";
import { IS_APPLE } from "../../utils/detectMac.js";
import { getSelectionRectangle, getSelectedNode } from "../../utils/lexicalHelpers.js";
import { createActiveEditorSubscription$, viewMode$, readOnly$, activeEditor$, currentSelection$, addComposerChild$ } from "../core/index.js";
import { LinkDialog } from "./LinkDialog.js";
import { $findMatchingParent } from "@lexical/utils";
function getLinkNodeInSelection(selection) {
if (!selection) {
return null;
}
const node = getSelectedNode(selection);
if (node === null) {
return null;
}
const parent = node.getParent();
if ($isLinkNode(parent)) {
return parent;
} else if ($isLinkNode(node)) {
return node;
}
return null;
}
const onWindowChange$ = Signal();
const linkDialogState$ = Cell({ type: "inactive" }, (r) => {
r.pub(createActiveEditorSubscription$, (editor) => {
return editor.registerCommand(
KEY_ESCAPE_COMMAND,
() => {
const state = r.getValue(linkDialogState$);
if (state.type === "preview") {
r.pub(linkDialogState$, { type: "inactive" });
return true;
}
return false;
},
COMMAND_PRIORITY_LOW
);
});
r.sub(r.pipe(viewMode$), (viewMode) => {
if (viewMode !== "rich-text") {
r.pub(linkDialogState$, { type: "inactive" });
}
});
r.pub(createActiveEditorSubscription$, (editor) => {
return editor.registerCommand(
KEY_MODIFIER_COMMAND,
(event) => {
if (event.key === "k" && (IS_APPLE ? event.metaKey : event.ctrlKey) && !r.getValue(readOnly$)) {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
r.pub(openLinkEditDialog$);
event.stopPropagation();
event.preventDefault();
return true;
} else {
return false;
}
}
return false;
},
COMMAND_PRIORITY_HIGH
);
});
r.sub(r.pipe(switchFromPreviewToLinkEdit$, withLatestFrom(linkDialogState$, activeEditor$)), ([, state, editor]) => {
if (state.type === "preview") {
setTimeout(() => {
editor == null ? void 0 : editor.getEditorState().read(() => {
const node = $getNodeByKey(state.linkNodeKey);
const withAnchorText = $isLinkNode(node) ? node.getTextContent().length > 0 && node.getChildrenSize() <= 1 : false;
const text = withAnchorText && node ? node.getTextContent() : "";
r.pub(linkDialogState$, {
type: "edit",
initialUrl: state.url,
url: state.url,
title: state.title,
text,
withAnchorText,
linkNodeKey: state.linkNodeKey,
rectangle: state.rectangle
});
});
});
} else {
throw new Error("Cannot switch to edit mode when not in preview mode");
}
});
r.sub(r.pipe(updateLink$, withLatestFrom(activeEditor$, linkDialogState$, currentSelection$)), ([payload, editor, state, selection]) => {
var _a, _b, _c;
const text = ((_a = payload.text) == null ? void 0 : _a.trim()) ?? "";
const url = ((_b = payload.url) == null ? void 0 : _b.trim()) ?? "";
const title = ((_c = payload.title) == null ? void 0 : _c.trim()) ?? "";
if (url !== "") {
if (selection == null ? void 0 : selection.isCollapsed()) {
const linkContent = text || title || url;
editor == null ? void 0 : editor.update(
() => {
const linkNode = getLinkNodeInSelection(selection);
if (!linkNode) {
const node = $createLinkNode(url, { title });
node.append($createTextNode(linkContent));
$insertNodes([node]);
node.select();
} else {
linkNode.setURL(url);
linkNode.setTitle(title);
updateLinkText(linkNode.getFirstChild(), text);
}
},
{ discrete: true }
);
} else {
editor == null ? void 0 : editor.update(() => {
updateLinkText(selection == null ? void 0 : selection.anchor.getNode(), text);
});
editor == null ? void 0 : editor.dispatchCommand(TOGGLE_LINK_COMMAND, { url, title });
}
r.pub(linkDialogState$, {
type: "preview",
linkNodeKey: state.linkNodeKey,
rectangle: state.rectangle,
title,
url
});
} else {
if (state.type === "edit" && state.initialUrl !== "") {
editor == null ? void 0 : editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}
r.pub(linkDialogState$, {
type: "inactive"
});
}
});
r.link(
r.pipe(
cancelLinkEdit$,
withLatestFrom(linkDialogState$, activeEditor$),
map(([, state, editor]) => {
if (state.type === "edit") {
editor == null ? void 0 : editor.focus();
if (state.initialUrl === "") {
return {
type: "inactive"
};
} else {
return {
type: "preview",
url: state.initialUrl,
linkNodeKey: state.linkNodeKey,
rectangle: state.rectangle
};
}
} else {
throw new Error("Cannot cancel edit when not in edit mode");
}
})
),
linkDialogState$
);
r.link(
r.pipe(
r.combine(currentSelection$, onWindowChange$),
withLatestFrom(activeEditor$, linkDialogState$, readOnly$),
map(([[selection], activeEditor, _, readOnly]) => {
if ($isRangeSelection(selection) && activeEditor && !readOnly) {
const node = getLinkNodeInSelection(selection);
if (node) {
const rect = getSelectionRectangle(activeEditor);
if (!rect) {
return { type: "inactive" };
}
return {
type: "preview",
url: node.getURL(),
linkNodeKey: node.getKey(),
title: node.getTitle(),
rectangle: rect
};
} else {
return { type: "inactive" };
}
} else {
return { type: "inactive" };
}
})
),
linkDialogState$
);
});
const updateLink$ = Signal();
const cancelLinkEdit$ = Action();
const applyLinkChanges$ = Action();
const switchFromPreviewToLinkEdit$ = Action();
const removeLink$ = Action((r) => {
r.sub(r.pipe(removeLink$, withLatestFrom(activeEditor$)), ([, editor]) => {
editor == null ? void 0 : editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
});
});
const openLinkEditDialog$ = Action((r) => {
r.sub(
r.pipe(
openLinkEditDialog$,
withLatestFrom(currentSelection$, activeEditor$),
filter(([, selection]) => $isRangeSelection(selection))
),
([, selection, editor]) => {
editor == null ? void 0 : editor.focus(() => {
setTimeout(() => {
editor.getEditorState().read(() => {
const linkNode = getLinkNodeInSelection(selection);
const rectangle = getSelectionRectangle(editor);
const initialUrl = (linkNode == null ? void 0 : linkNode.getURL()) ?? "";
const url = (linkNode == null ? void 0 : linkNode.getURL()) ?? "";
const title = (linkNode == null ? void 0 : linkNode.getTitle()) ?? "";
const linkNodeKey = (linkNode == null ? void 0 : linkNode.getKey()) ?? "";
const withAnchorText = linkNode ? linkNode.getTextContent().length > 0 && linkNode.getChildrenSize() <= 1 : Boolean(selection == null ? void 0 : selection.isCollapsed());
const text = withAnchorText && linkNode ? linkNode.getTextContent() : "";
r.pub(linkDialogState$, {
type: "edit",
initialUrl,
url,
title,
text,
withAnchorText,
linkNodeKey,
rectangle
});
});
});
});
}
);
});
const linkAutocompleteSuggestions$ = Cell([]);
const onClickLinkCallback$ = Cell(null);
const onReadOnlyClickLinkCallback$ = Cell(null, (r) => {
r.pub(createActiveEditorSubscription$, (editor) => {
function onClick(event) {
const [readOnly, callback] = r.getValues([readOnly$, onReadOnlyClickLinkCallback$]);
if (!readOnly || callback === null) {
return;
}
editor.update(() => {
const nearestNode = $getNearestNodeFromDOMNode(event.target);
if (nearestNode !== null) {
const targetNode = $findMatchingParent(nearestNode, (node) => node instanceof LinkNode);
if (targetNode !== null) {
callback(event, targetNode, targetNode.getURL());
}
}
});
}
return editor.registerRootListener((rootElement, prevRoot) => {
if (rootElement) {
rootElement.addEventListener("click", onClick);
}
if (prevRoot) {
prevRoot.removeEventListener("click", onClick);
}
});
});
});
function updateLinkText(node, text) {
if ($isTextNode(node) && text) {
node.setTextContent(text);
node.selectStart();
}
}
const showLinkTitleField$ = Cell(true);
const linkDialogPlugin = realmPlugin({
init(r, params) {
r.pub(addComposerChild$, (params == null ? void 0 : params.LinkDialog) ?? LinkDialog);
r.pub(onClickLinkCallback$, (params == null ? void 0 : params.onClickLinkCallback) ?? null);
r.pub(onReadOnlyClickLinkCallback$, (params == null ? void 0 : params.onReadOnlyClickLinkCallback) ?? null);
r.pub(showLinkTitleField$, (params == null ? void 0 : params.showLinkTitleField) ?? true);
},
update(r, params = {}) {
r.pub(linkAutocompleteSuggestions$, params.linkAutocompleteSuggestions ?? []);
}
});
export {
applyLinkChanges$,
cancelLinkEdit$,
linkAutocompleteSuggestions$,
linkDialogPlugin,
linkDialogState$,
onClickLinkCallback$,
onReadOnlyClickLinkCallback$,
onWindowChange$,
openLinkEditDialog$,
removeLink$,
showLinkTitleField$,
switchFromPreviewToLinkEdit$,
updateLink$
};