@mdxeditor/editor 4.1.1 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/MDXEditor.js CHANGED
@@ -35,7 +35,7 @@ const RichTextEditor = () => {
35
35
  return /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, topAreaChildren.map((Child, index) => /* @__PURE__ */ React__default.createElement(Child, { key: index })), /* @__PURE__ */ React__default.createElement(RenderRecursiveWrappers, { wrappers: editorWrappers }, /* @__PURE__ */ React__default.createElement("div", { className: classNames(styles.rootContentEditableWrapper, "mdxeditor-root-contenteditable") }, /* @__PURE__ */ React__default.createElement(
36
36
  RichTextPlugin,
37
37
  {
38
- contentEditable: /* @__PURE__ */ React__default.createElement("div", { ref: onRef }, /* @__PURE__ */ React__default.createElement(
38
+ contentEditable: /* @__PURE__ */ React__default.createElement("div", { ref: onRef, className: "mdxeditor-contenteditable-wrapper" }, /* @__PURE__ */ React__default.createElement(
39
39
  ContentEditable,
40
40
  {
41
41
  className: classNames(styles.contentEditable, contentEditableClassName),
package/dist/index.d.ts CHANGED
@@ -1160,6 +1160,8 @@ export declare interface EditLinkDialog {
1160
1160
  text: string;
1161
1161
  withAnchorText: boolean;
1162
1162
  linkNodeKey: string;
1163
+ /** Set when the dialog targets a single inline decorator node (e.g. an image) instead of a text selection. */
1164
+ decoratorNodeKey?: string;
1163
1165
  rectangle: RectData;
1164
1166
  }
1165
1167
 
@@ -1309,6 +1311,7 @@ export declare class FrontmatterNode extends DecoratorNode<JSX.Element> {
1309
1311
  setYaml(yaml: string): void;
1310
1312
  decorate(editor: LexicalEditor): JSX.Element;
1311
1313
  isKeyboardSelectable(): boolean;
1314
+ isInline(): boolean;
1312
1315
  }
1313
1316
 
1314
1317
  /**
@@ -1385,6 +1388,17 @@ export declare function getCodeBlockLanguageSelectData(normalized: NormalizedCod
1385
1388
  }[];
1386
1389
  };
1387
1390
 
1391
+ /**
1392
+ * Gets the coordinates of the DOM element that renders the node with the given key.
1393
+ * @group Utils
1394
+ */
1395
+ export declare function getNodeRectangle(editor: LexicalEditor, nodeKey: string): {
1396
+ top: number;
1397
+ left: number;
1398
+ width: number;
1399
+ height: number;
1400
+ } | null;
1401
+
1388
1402
  /**
1389
1403
  * Gets the selected node from the Lexical editor.
1390
1404
  * @group Utils
@@ -1591,6 +1605,7 @@ export declare interface InactiveLinkDialog {
1591
1605
  type: 'inactive';
1592
1606
  rectangle?: undefined;
1593
1607
  linkNodeKey?: undefined;
1608
+ decoratorNodeKey?: undefined;
1594
1609
  }
1595
1610
 
1596
1611
  /**
@@ -2623,6 +2638,8 @@ export declare interface PreviewLinkDialog {
2623
2638
  /** The fail-closed URL used only for automatic preview navigation. */
2624
2639
  href?: string;
2625
2640
  linkNodeKey: string;
2641
+ /** Set when the link wraps a single inline decorator node (e.g. an image) that the dialog targets. */
2642
+ decoratorNodeKey?: string;
2626
2643
  rectangle: RectData;
2627
2644
  }
2628
2645
 
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ import { RemoteMDXEditorRealmProvider, remoteRealmPlugin, useRemoteMDXEditorReal
54
54
  import { CAN_USE_DOM, IS_APPLE, controlOrMeta } from "./utils/detectMac.js";
55
55
  import { always, call, compose, curry1to0, curry2to1, joinProc, noop, prop, tap, thrush } from "./utils/fp.js";
56
56
  import { isPartOftheEditorUI } from "./utils/isPartOftheEditorUI.js";
57
- import { fromWithinEditorRead, getSelectedNode, getSelectionAsMarkdown, getSelectionRectangle, getStateAsMarkdown } from "./utils/lexicalHelpers.js";
57
+ import { fromWithinEditorRead, getNodeRectangle, getSelectedNode, getSelectionAsMarkdown, getSelectionRectangle, getStateAsMarkdown } from "./utils/lexicalHelpers.js";
58
58
  import { makeHslTransparent } from "./utils/makeHslTransparent.js";
59
59
  import { uuidv4 } from "./utils/uuid4.js";
60
60
  import { voidEmitter } from "./utils/voidEmitter.js";
@@ -242,6 +242,7 @@ export {
242
242
  frontmatterDialogOpen$,
243
243
  frontmatterPlugin,
244
244
  getCodeBlockLanguageSelectData,
245
+ getNodeRectangle,
245
246
  getSelectedNode,
246
247
  getSelectionAsMarkdown,
247
248
  getSelectionRectangle,
@@ -59,6 +59,9 @@ class FrontmatterNode extends DecoratorNode {
59
59
  isKeyboardSelectable() {
60
60
  return false;
61
61
  }
62
+ isInline() {
63
+ return false;
64
+ }
62
65
  }
63
66
  function $createFrontmatterNode(yaml) {
64
67
  return new FrontmatterNode(yaml);
@@ -1,7 +1,7 @@
1
1
  import * as RadixPopover from "@radix-ui/react-popover";
2
2
  import * as Tooltip from "@radix-ui/react-tooltip";
3
3
  import React__default from "react";
4
- import { editorRootElementRef$, activeEditor$, iconComponentFor$, useTranslation } from "../core/index.js";
4
+ import { editorRootElementRef$, contentEditableWrapperElement$, activeEditor$, iconComponentFor$, useTranslation } from "../core/index.js";
5
5
  import { DownshiftAutoComplete } from "../core/ui/DownshiftAutoComplete.js";
6
6
  import styles from "../../styles/ui.module.css.js";
7
7
  import classNames from "classnames";
@@ -93,6 +93,7 @@ function LinkEditForm({
93
93
  const LinkDialog = () => {
94
94
  const [
95
95
  editorRootElementRef,
96
+ contentEditableWrapperElement,
96
97
  activeEditor,
97
98
  iconComponentFor,
98
99
  linkDialogState,
@@ -101,6 +102,7 @@ const LinkDialog = () => {
101
102
  showLinkTitleField
102
103
  ] = useCellValues(
103
104
  editorRootElementRef$,
105
+ contentEditableWrapperElement$,
104
106
  activeEditor$,
105
107
  iconComponentFor$,
106
108
  linkDialogState$,
@@ -114,6 +116,7 @@ const LinkDialog = () => {
114
116
  const switchFromPreviewToLinkEdit = usePublisher(switchFromPreviewToLinkEdit$);
115
117
  const removeLink = usePublisher(removeLink$);
116
118
  React__default.useEffect(() => {
119
+ const scrollContainer = contentEditableWrapperElement == null ? void 0 : contentEditableWrapperElement.closest(".mdxeditor-root-contenteditable");
117
120
  const update = () => {
118
121
  activeEditor == null ? void 0 : activeEditor.getEditorState().read(() => {
119
122
  publishWindowChange(true);
@@ -121,11 +124,13 @@ const LinkDialog = () => {
121
124
  };
122
125
  window.addEventListener("resize", update);
123
126
  window.addEventListener("scroll", update);
127
+ scrollContainer == null ? void 0 : scrollContainer.addEventListener("scroll", update);
124
128
  return () => {
125
129
  window.removeEventListener("resize", update);
126
130
  window.removeEventListener("scroll", update);
131
+ scrollContainer == null ? void 0 : scrollContainer.removeEventListener("scroll", update);
127
132
  };
128
- }, [activeEditor, publishWindowChange]);
133
+ }, [activeEditor, contentEditableWrapperElement, publishWindowChange]);
129
134
  const [copyUrlTooltipOpen, setCopyUrlTooltipOpen] = React__default.useState(false);
130
135
  const t = useTranslation();
131
136
  if (linkDialogState.type === "inactive") return null;
@@ -148,6 +153,7 @@ const LinkDialog = () => {
148
153
  {
149
154
  className: classNames(styles.linkDialogPopoverContent),
150
155
  sideOffset: 5,
156
+ updatePositionStrategy: "always",
151
157
  onOpenAutoFocus: (e) => {
152
158
  e.preventDefault();
153
159
  },
@@ -1,9 +1,9 @@
1
- import { $isLinkNode, $createLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND, LinkNode } from "@lexical/link";
2
- import { Action, Cell, withLatestFrom, map, Signal, filter } from "@mdxeditor/gurx";
3
- import { KEY_ESCAPE_COMMAND, COMMAND_PRIORITY_LOW, KEY_DOWN_COMMAND, $getSelection, $isRangeSelection, COMMAND_PRIORITY_HIGH, $getNodeByKey, $createTextNode, $insertNodes, $isTextNode, $getNearestNodeFromDOMNode } from "lexical";
1
+ import { $isLinkNode, TOGGLE_LINK_COMMAND, $createLinkNode, $isAutoLinkNode, LinkNode } from "@lexical/link";
2
+ import { Action, Cell, withLatestFrom, map, Signal } from "@mdxeditor/gurx";
3
+ import { KEY_ESCAPE_COMMAND, COMMAND_PRIORITY_LOW, KEY_DOWN_COMMAND, $getSelection, $isRangeSelection, COMMAND_PRIORITY_HIGH, $getNodeByKey, $createNodeSelection, $setSelection, $createTextNode, $insertNodes, $isNodeSelection, $isDecoratorNode, $isTextNode, $getNearestNodeFromDOMNode } from "lexical";
4
4
  import { realmPlugin } from "../../RealmWithPlugins.js";
5
5
  import { IS_APPLE } from "../../utils/detectMac.js";
6
- import { getSelectionRectangle, getSelectedNode } from "../../utils/lexicalHelpers.js";
6
+ import { getNodeRectangle, getSelectionRectangle, getSelectedNode } from "../../utils/lexicalHelpers.js";
7
7
  import { createActiveEditorSubscription$, viewMode$, readOnly$, activeEditor$, currentSelection$, addComposerChild$ } from "../core/index.js";
8
8
  import { LinkDialog } from "./LinkDialog.js";
9
9
  import { $findMatchingParent } from "@lexical/utils";
@@ -23,6 +23,14 @@ function getLinkNodeInSelection(selection) {
23
23
  }
24
24
  return null;
25
25
  }
26
+ function getSelectedInlineDecorator(selection = $getSelection()) {
27
+ if (!$isNodeSelection(selection)) {
28
+ return null;
29
+ }
30
+ const nodes = selection.getNodes();
31
+ const node = nodes.length === 1 ? nodes[0] : null;
32
+ return $isDecoratorNode(node) && node.isInline() ? node : null;
33
+ }
26
34
  function getPreviewHref(editor, linkNodeKey, url) {
27
35
  if (!editor) return "about:blank";
28
36
  return editor.getEditorState().read(() => {
@@ -60,7 +68,7 @@ const linkDialogState$ = Cell({ type: "inactive" }, (r) => {
60
68
  (event) => {
61
69
  if (event.key === "k" && (IS_APPLE ? event.metaKey : event.ctrlKey) && !r.getValue(readOnly$)) {
62
70
  const selection = $getSelection();
63
- if ($isRangeSelection(selection)) {
71
+ if ($isRangeSelection(selection) || getSelectedInlineDecorator(selection) !== null) {
64
72
  r.pub(openLinkEditDialog$);
65
73
  event.stopPropagation();
66
74
  event.preventDefault();
@@ -89,6 +97,7 @@ const linkDialogState$ = Cell({ type: "inactive" }, (r) => {
89
97
  text,
90
98
  withAnchorText,
91
99
  linkNodeKey: state.linkNodeKey,
100
+ decoratorNodeKey: state.decoratorNodeKey,
92
101
  rectangle: state.rectangle
93
102
  });
94
103
  });
@@ -103,6 +112,44 @@ const linkDialogState$ = Cell({ type: "inactive" }, (r) => {
103
112
  const url = ((_b = payload.url) == null ? void 0 : _b.trim()) ?? "";
104
113
  const title = ((_c = payload.title) == null ? void 0 : _c.trim()) ?? "";
105
114
  if (url !== "") {
115
+ if (state.decoratorNodeKey !== void 0) {
116
+ let linkNodeKey = "";
117
+ let linkApplied = false;
118
+ editor == null ? void 0 : editor.update(
119
+ () => {
120
+ const node = $getNodeByKey(state.decoratorNodeKey);
121
+ if (node === null) {
122
+ return;
123
+ }
124
+ const nodeSelection = $createNodeSelection();
125
+ nodeSelection.add(node.getKey());
126
+ $setSelection(nodeSelection);
127
+ linkApplied = editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
128
+ if (!linkApplied) {
129
+ return;
130
+ }
131
+ const parent = node.getParent();
132
+ if ($isLinkNode(parent)) {
133
+ parent.setTitle(title);
134
+ linkNodeKey = parent.getKey();
135
+ }
136
+ },
137
+ { discrete: true }
138
+ );
139
+ if (linkNodeKey === "") {
140
+ return;
141
+ }
142
+ r.pub(linkDialogState$, {
143
+ type: "preview",
144
+ linkNodeKey,
145
+ decoratorNodeKey: state.decoratorNodeKey,
146
+ rectangle: state.rectangle,
147
+ title,
148
+ url,
149
+ href: getPreviewHref(editor, linkNodeKey, url)
150
+ });
151
+ return;
152
+ }
106
153
  if (selection == null ? void 0 : selection.isCollapsed()) {
107
154
  const linkContent = text || title || url;
108
155
  editor == null ? void 0 : editor.update(
@@ -144,7 +191,20 @@ const linkDialogState$ = Cell({ type: "inactive" }, (r) => {
144
191
  });
145
192
  } else {
146
193
  if (state.type === "edit" && state.initialUrl !== "") {
147
- editor == null ? void 0 : editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
194
+ if (state.decoratorNodeKey !== void 0) {
195
+ editor == null ? void 0 : editor.update(() => {
196
+ const node = $getNodeByKey(state.decoratorNodeKey);
197
+ if (node === null) {
198
+ return;
199
+ }
200
+ const nodeSelection = $createNodeSelection();
201
+ nodeSelection.add(node.getKey());
202
+ $setSelection(nodeSelection);
203
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
204
+ });
205
+ } else {
206
+ editor == null ? void 0 : editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
207
+ }
148
208
  }
149
209
  r.pub(linkDialogState$, {
150
210
  type: "inactive"
@@ -168,6 +228,7 @@ const linkDialogState$ = Cell({ type: "inactive" }, (r) => {
168
228
  url: state.initialUrl,
169
229
  href: getPreviewHref(editor, state.linkNodeKey, state.initialUrl),
170
230
  linkNodeKey: state.linkNodeKey,
231
+ decoratorNodeKey: state.decoratorNodeKey,
171
232
  rectangle: state.rectangle
172
233
  };
173
234
  }
@@ -182,7 +243,22 @@ const linkDialogState$ = Cell({ type: "inactive" }, (r) => {
182
243
  r.pipe(
183
244
  r.combine(currentSelection$, onWindowChange$),
184
245
  withLatestFrom(activeEditor$, linkDialogState$, readOnly$),
185
- map(([[selection], activeEditor, _, readOnly]) => {
246
+ map(([[selection], activeEditor, state, readOnly]) => {
247
+ if (state.decoratorNodeKey !== void 0 && activeEditor && !readOnly) {
248
+ const selectedDecoratorKey = activeEditor.getEditorState().read(() => {
249
+ const editorSelection = $getSelection();
250
+ if (!$isNodeSelection(editorSelection)) {
251
+ return null;
252
+ }
253
+ const nodes = editorSelection.getNodes();
254
+ return nodes.length === 1 ? nodes[0].getKey() : null;
255
+ });
256
+ if (selectedDecoratorKey !== state.decoratorNodeKey) {
257
+ return { type: "inactive" };
258
+ }
259
+ const rectangle = getNodeRectangle(activeEditor, state.decoratorNodeKey);
260
+ return rectangle === null ? { type: "inactive" } : { ...state, rectangle };
261
+ }
186
262
  if ($isRangeSelection(selection) && activeEditor && !readOnly) {
187
263
  const node = getLinkNodeInSelection(selection);
188
264
  if (!selection.isCollapsed()) return { type: "inactive" };
@@ -220,39 +296,73 @@ const removeLink$ = Action((r) => {
220
296
  });
221
297
  });
222
298
  const openLinkEditDialog$ = Action((r) => {
223
- r.sub(
224
- r.pipe(
225
- openLinkEditDialog$,
226
- withLatestFrom(currentSelection$, activeEditor$),
227
- filter(([, selection]) => $isRangeSelection(selection))
228
- ),
229
- ([, selection, editor]) => {
230
- editor == null ? void 0 : editor.focus(() => {
231
- setTimeout(() => {
232
- editor.getEditorState().read(() => {
233
- const linkNode = getLinkNodeInSelection(selection);
234
- const rectangle = getSelectionRectangle(editor);
235
- const initialUrl = (linkNode == null ? void 0 : linkNode.getURL()) ?? "";
236
- const url = (linkNode == null ? void 0 : linkNode.getURL()) ?? "";
237
- const title = (linkNode == null ? void 0 : linkNode.getTitle()) ?? "";
238
- const linkNodeKey = (linkNode == null ? void 0 : linkNode.getKey()) ?? "";
239
- const withAnchorText = linkNode ? linkNode.getTextContent().length > 0 && linkNode.getChildrenSize() <= 1 : Boolean(selection == null ? void 0 : selection.isCollapsed());
240
- const text = withAnchorText && linkNode ? linkNode.getTextContent() : "";
241
- r.pub(linkDialogState$, {
242
- type: "edit",
243
- initialUrl,
244
- url,
245
- title,
246
- text,
247
- withAnchorText,
248
- linkNodeKey,
249
- rectangle
250
- });
299
+ r.sub(r.pipe(openLinkEditDialog$, withLatestFrom(currentSelection$, activeEditor$)), ([, selection, editor]) => {
300
+ if (!editor) {
301
+ return;
302
+ }
303
+ const decorator = editor.getEditorState().read(() => {
304
+ const node = getSelectedInlineDecorator();
305
+ if (node === null) {
306
+ return null;
307
+ }
308
+ const parent = node.getParent();
309
+ const linkNode = $isLinkNode(parent) ? parent : null;
310
+ return {
311
+ nodeKey: node.getKey(),
312
+ url: (linkNode == null ? void 0 : linkNode.getURL()) ?? "",
313
+ title: (linkNode == null ? void 0 : linkNode.getTitle()) ?? "",
314
+ linkNodeKey: (linkNode == null ? void 0 : linkNode.getKey()) ?? ""
315
+ };
316
+ });
317
+ if (decorator !== null) {
318
+ const rectangle = getNodeRectangle(editor, decorator.nodeKey);
319
+ if (rectangle === null) {
320
+ return;
321
+ }
322
+ r.pub(linkDialogState$, {
323
+ type: "edit",
324
+ initialUrl: decorator.url,
325
+ url: decorator.url,
326
+ title: decorator.title,
327
+ text: "",
328
+ withAnchorText: false,
329
+ linkNodeKey: decorator.linkNodeKey,
330
+ decoratorNodeKey: decorator.nodeKey,
331
+ rectangle
332
+ });
333
+ return;
334
+ }
335
+ if (!$isRangeSelection(selection)) {
336
+ return;
337
+ }
338
+ editor.focus(() => {
339
+ setTimeout(() => {
340
+ editor.getEditorState().read(() => {
341
+ const linkNode = getLinkNodeInSelection(selection);
342
+ const rectangle = getSelectionRectangle(editor);
343
+ if (rectangle === null) {
344
+ return;
345
+ }
346
+ const initialUrl = (linkNode == null ? void 0 : linkNode.getURL()) ?? "";
347
+ const url = (linkNode == null ? void 0 : linkNode.getURL()) ?? "";
348
+ const title = (linkNode == null ? void 0 : linkNode.getTitle()) ?? "";
349
+ const linkNodeKey = (linkNode == null ? void 0 : linkNode.getKey()) ?? "";
350
+ const withAnchorText = linkNode ? linkNode.getTextContent().length > 0 && linkNode.getChildrenSize() <= 1 : selection.isCollapsed();
351
+ const text = withAnchorText && linkNode ? linkNode.getTextContent() : "";
352
+ r.pub(linkDialogState$, {
353
+ type: "edit",
354
+ initialUrl,
355
+ url,
356
+ title,
357
+ text,
358
+ withAnchorText,
359
+ linkNodeKey,
360
+ rectangle
251
361
  });
252
362
  });
253
363
  });
254
- }
255
- );
364
+ });
365
+ });
256
366
  });
257
367
  const linkAutocompleteSuggestions$ = Cell([]);
258
368
  const onClickLinkCallback$ = Cell(null);
@@ -12,6 +12,9 @@ const CreateLink = () => {
12
12
  {
13
13
  "aria-label": t("toolbar.link", "Create link"),
14
14
  title: t("toolbar.link", "Create link"),
15
+ onPointerDown: (event) => {
16
+ event.preventDefault();
17
+ },
15
18
  onClick: (_) => {
16
19
  openLinkDialog();
17
20
  }