@octanejs/lexical 0.1.9 → 0.1.10

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.
Files changed (34) hide show
  1. package/package.json +5 -5
  2. package/src/LexicalAutoEmbedPlugin.tsrx +31 -6
  3. package/src/LexicalAutoFocusPlugin.tsrx +5 -1
  4. package/src/LexicalAutoLinkPlugin.tsrx +9 -1
  5. package/src/LexicalBlockWithAlignableContents.tsrx +13 -1
  6. package/src/LexicalCharacterLimitPlugin.tsrx +18 -7
  7. package/src/LexicalCheckListPlugin.tsrx +5 -1
  8. package/src/LexicalClearEditorPlugin.tsrx +5 -1
  9. package/src/LexicalClickableLinkPlugin.tsrx +6 -1
  10. package/src/LexicalComposer.tsrx +13 -4
  11. package/src/LexicalContentEditable.tsrx +25 -3
  12. package/src/LexicalDraggableBlockPlugin.tsrx +68 -31
  13. package/src/LexicalEditorRefPlugin.tsrx +7 -1
  14. package/src/LexicalErrorBoundary.tsrx +18 -2
  15. package/src/LexicalHistoryPlugin.tsrx +7 -1
  16. package/src/LexicalHorizontalRuleNode.tsrx +10 -7
  17. package/src/LexicalLinkPlugin.tsrx +7 -1
  18. package/src/LexicalListPlugin.tsrx +17 -1
  19. package/src/LexicalMarkdownShortcutPlugin.tsrx +8 -3
  20. package/src/LexicalNestedComposer.tsrx +21 -3
  21. package/src/LexicalNodeContextMenuPlugin.tsrx +76 -22
  22. package/src/LexicalNodeEventPlugin.tsrx +10 -3
  23. package/src/LexicalNodeMenuPlugin.tsrx +21 -3
  24. package/src/LexicalOnChangePlugin.tsrx +8 -1
  25. package/src/LexicalPlainTextPlugin.tsrx +16 -2
  26. package/src/LexicalRichTextPlugin.tsrx +16 -2
  27. package/src/LexicalSelectionAlwaysOnDisplay.tsrx +5 -1
  28. package/src/LexicalTabIndentationPlugin.tsrx +12 -1
  29. package/src/LexicalTableOfContentsPlugin.tsrx +39 -12
  30. package/src/LexicalTablePlugin.tsrx +34 -1
  31. package/src/LexicalTypeaheadMenuPlugin.tsrx +24 -3
  32. package/src/shared/LexicalContentEditableElement.tsrx +58 -10
  33. package/src/shared/LexicalDecorators.tsrx +28 -5
  34. package/src/shared/LexicalMenu.tsrx +38 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/lexical",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -105,19 +105,19 @@
105
105
  "@lexical/yjs": "0.46.0",
106
106
  "lexical": "0.46.0",
107
107
  "yjs": "^13.6.0",
108
- "@octanejs/floating-ui": "0.1.9"
108
+ "@octanejs/floating-ui": "0.1.10"
109
109
  },
110
110
  "peerDependencies": {
111
- "octane": "0.1.10"
111
+ "octane": "0.1.11"
112
112
  },
113
113
  "devDependencies": {
114
114
  "@lexical/react": "0.46.0",
115
- "@tsrx/react": "^0.2.37",
115
+ "@tsrx/react": "^0.2.44",
116
116
  "esbuild": "^0.28.1",
117
117
  "react": "^19.2.0",
118
118
  "react-dom": "^19.2.0",
119
119
  "vitest": "^4.1.9",
120
- "octane": "0.1.10"
120
+ "octane": "0.1.11"
121
121
  },
122
122
  "scripts": {
123
123
  "test": "vitest run"
@@ -3,8 +3,12 @@
3
3
  // node menu offering to replace the link with an embed node. Built on the ported
4
4
  // LexicalNodeMenuPlugin. Typed surface + AutoEmbedOption live in autoEmbedShared.ts.
5
5
  import { $isLinkNode, AutoLinkNode, LinkNode } from '@lexical/link';
6
+ import type { AutoEmbedOption, EmbedConfig } from './autoEmbedShared';
7
+ import { INSERT_EMBED_COMMAND } from './autoEmbedShared';
6
8
  import { useLexicalComposerContext } from './LexicalComposerContext';
7
9
  import { LexicalNodeMenuPlugin } from './LexicalNodeMenuPlugin.tsrx';
10
+ import type { MenuRenderFn } from './shared/menuShared';
11
+ import type { CommandListenerPriority, LexicalNode, NodeKey, NodeMutation } from 'lexical';
8
12
  import {
9
13
  $getNodeByKey,
10
14
  $getSelection,
@@ -17,7 +21,15 @@ import { useCallback, useEffect, useMemo, useState } from 'octane';
17
21
 
18
22
  export { URL_MATCHER, INSERT_EMBED_COMMAND, AutoEmbedOption } from './autoEmbedShared';
19
23
 
20
- export function LexicalAutoEmbedPlugin(props) {
24
+ type LexicalAutoEmbedPluginProps<TEmbedConfig extends EmbedConfig> = {
25
+ embedConfigs: TEmbedConfig[];
26
+ onOpenEmbedModalForConfig?: (embedConfig: TEmbedConfig) => void;
27
+ getMenuOptions: (activeEmbedConfig: TEmbedConfig, embedFn: () => void, dismissFn: () => void) => AutoEmbedOption[];
28
+ menuRenderFn?: MenuRenderFn<AutoEmbedOption>;
29
+ menuCommandPriority?: CommandListenerPriority;
30
+ };
31
+
32
+ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>(props: LexicalAutoEmbedPluginProps<TEmbedConfig>) {
21
33
  const embedConfigs = props.embedConfigs;
22
34
  const onOpenEmbedModalForConfig = props.onOpenEmbedModalForConfig;
23
35
  const getMenuOptions = props.getMenuOptions;
@@ -27,15 +39,15 @@ export function LexicalAutoEmbedPlugin(props) {
27
39
 
28
40
  const [editor] = useLexicalComposerContext();
29
41
 
30
- const [nodeKey, setNodeKey] = useState(null);
31
- const [activeEmbedConfig, setActiveEmbedConfig] = useState(null);
42
+ const [nodeKey, setNodeKey] = useState<NodeKey | null>(null);
43
+ const [activeEmbedConfig, setActiveEmbedConfig] = useState<TEmbedConfig | null>(null);
32
44
 
33
45
  const reset = useCallback(() => {
34
46
  setNodeKey(null);
35
47
  setActiveEmbedConfig(null);
36
48
  }, []);
37
49
 
38
- const checkIfLinkNodeIsEmbeddable = useCallback(async (key) => {
50
+ const checkIfLinkNodeIsEmbeddable = useCallback(async (key: NodeKey) => {
39
51
  const url = editor.read('latest', function () {
40
52
  const linkNode = $getNodeByKey(key);
41
53
  if ($isLinkNode(linkNode)) {
@@ -55,7 +67,16 @@ export function LexicalAutoEmbedPlugin(props) {
55
67
  }, [editor, embedConfigs]);
56
68
 
57
69
  useEffect(() => {
58
- const listener = (nodeMutations, { updateTags, dirtyLeaves }) => {
70
+ const listener = (
71
+ nodeMutations: Map<NodeKey, NodeMutation>,
72
+ {
73
+ updateTags,
74
+ dirtyLeaves,
75
+ }: {
76
+ updateTags: Set<string>;
77
+ dirtyLeaves: Set<string>;
78
+ },
79
+ ) => {
59
80
  for (const [key, mutation] of nodeMutations) {
60
81
  if (mutation === 'created' && updateTags.has(PASTE_TAG) && dirtyLeaves.size <= 3) {
61
82
  checkIfLinkNodeIsEmbeddable(key);
@@ -123,7 +144,11 @@ export function LexicalAutoEmbedPlugin(props) {
123
144
  : [];
124
145
  }, [activeEmbedConfig, embedLinkViaActiveEmbedConfig, getMenuOptions, nodeKey, reset]);
125
146
 
126
- const onSelectOption = useCallback((selectedOption, targetNode, closeMenu) => {
147
+ const onSelectOption = useCallback((
148
+ selectedOption: AutoEmbedOption,
149
+ targetNode: LexicalNode | null,
150
+ closeMenu: () => void,
151
+ ) => {
127
152
  editor.update(() => {
128
153
  selectedOption.onSelect(targetNode);
129
154
  closeMenu();
@@ -3,7 +3,11 @@ import { useLexicalComposerContext } from './LexicalComposerContext';
3
3
  import { getActiveElement } from 'lexical';
4
4
  import { useEffect } from 'octane';
5
5
 
6
- export function AutoFocusPlugin(props) {
6
+ type Props = {
7
+ defaultSelection?: 'rootStart' | 'rootEnd';
8
+ };
9
+
10
+ export function AutoFocusPlugin(props: Props) {
7
11
  const [editor] = useLexicalComposerContext();
8
12
  const defaultSelection = props.defaultSelection;
9
13
 
@@ -1,12 +1,20 @@
1
1
  // Ported from @lexical/react/src/LexicalAutoLinkPlugin.ts (useAutoLink inlined).
2
2
  import invariant from '@lexical/internal/invariant';
3
3
  import { AutoLinkNode, registerAutoLink } from '@lexical/link';
4
+ import type { ChangeHandler, LinkMatcher } from '@lexical/link';
5
+ import type { ElementNode } from 'lexical';
4
6
  import { useLexicalComposerContext } from './LexicalComposerContext';
5
7
  import { useEffect } from 'octane';
6
8
 
7
9
  export { createLinkMatcherWithRegExp } from '@lexical/link';
8
10
 
9
- export function AutoLinkPlugin(props) {
11
+ type Props = {
12
+ matchers: LinkMatcher[];
13
+ onChange?: ChangeHandler;
14
+ excludeParents?: ((parent: ElementNode) => boolean)[];
15
+ };
16
+
17
+ export function AutoLinkPlugin(props: Props) {
10
18
  const [editor] = useLexicalComposerContext();
11
19
  const matchers = props.matchers;
12
20
  const onChange = props.onChange;
@@ -17,9 +17,21 @@ import {
17
17
  getComposedEventTarget,
18
18
  mergeRegister,
19
19
  } from 'lexical';
20
+ import type { ElementFormatType, NodeKey } from 'lexical';
20
21
  import { useEffect, useRef } from 'octane';
22
+ import type { OctaneNode } from 'octane';
21
23
 
22
- export function BlockWithAlignableContents(props) {
24
+ type Props = Readonly<{
25
+ children: OctaneNode;
26
+ format?: ElementFormatType | null;
27
+ nodeKey: NodeKey;
28
+ className: Readonly<{
29
+ base: string;
30
+ focus: string;
31
+ }>;
32
+ }>;
33
+
34
+ export function BlockWithAlignableContents(props: Props) {
23
35
  const nodeKey = props.nodeKey;
24
36
  const format = props.format;
25
37
  const className = props.className;
@@ -4,12 +4,13 @@
4
4
  // than called directly.
5
5
  import { useLexicalComposerContext } from './LexicalComposerContext';
6
6
  import { useCharacterLimit } from './shared/useCharacterLimit';
7
+ import type { OctaneNode } from 'octane';
7
8
  import { createElement, useMemo, useState } from 'octane';
8
9
 
9
10
  const CHARACTER_LIMIT = 5;
10
- let textEncoderInstance = null;
11
+ let textEncoderInstance: TextEncoder | null = null;
11
12
 
12
- function textEncoder() {
13
+ function textEncoder(): TextEncoder | null {
13
14
  if (window.TextEncoder === undefined) {
14
15
  return null;
15
16
  }
@@ -19,7 +20,7 @@ function textEncoder() {
19
20
  return textEncoderInstance;
20
21
  }
21
22
 
22
- function utf8Length(text) {
23
+ function utf8Length(text: string) {
23
24
  const currentTextEncoder = textEncoder();
24
25
  if (currentTextEncoder === null) {
25
26
  const m = encodeURIComponent(text).match(/%[89ABab]/g);
@@ -28,15 +29,25 @@ function utf8Length(text) {
28
29
  return currentTextEncoder.encode(text).length;
29
30
  }
30
31
 
31
- function DefaultRenderer(props) @{
32
+ type RendererProps = {
33
+ remainingCharacters: number;
34
+ };
35
+
36
+ function DefaultRenderer(props: RendererProps) @{
32
37
  <span
33
38
  class={'characters-limit ' + (props.remainingCharacters < 0 ? 'characters-limit-exceeded' : '')}
34
39
  >
35
- {props.remainingCharacters as string}
40
+ {props.remainingCharacters as unknown as string}
36
41
  </span>
37
42
  }
38
43
 
39
- export function CharacterLimitPlugin(props) {
44
+ type Props = {
45
+ charset?: 'UTF-8' | 'UTF-16';
46
+ maxLength?: number;
47
+ renderer?: (props: RendererProps) => OctaneNode;
48
+ };
49
+
50
+ export function CharacterLimitPlugin(props: Props) {
40
51
  const charset = props.charset ?? 'UTF-16';
41
52
  const maxLength = props.maxLength ?? CHARACTER_LIMIT;
42
53
  const renderer = props.renderer ?? DefaultRenderer;
@@ -47,7 +58,7 @@ export function CharacterLimitPlugin(props) {
47
58
  const characterLimitProps = useMemo(
48
59
  () => ({
49
60
  remainingCharacters: setRemainingCharacters,
50
- strlen: (text) => {
61
+ strlen: (text: string) => {
51
62
  if (charset === 'UTF-8') {
52
63
  return utf8Length(text);
53
64
  } else if (charset === 'UTF-16') {
@@ -3,7 +3,11 @@ import { registerCheckList } from '@lexical/list';
3
3
  import { useLexicalComposerContext } from './LexicalComposerContext';
4
4
  import { useEffect } from 'octane';
5
5
 
6
- export function CheckListPlugin(props) {
6
+ type Props = {
7
+ disableTakeFocusOnClick?: boolean;
8
+ };
9
+
10
+ export function CheckListPlugin(props: Props) {
7
11
  const [editor] = useLexicalComposerContext();
8
12
  const disableTakeFocusOnClick =
9
13
  props.disableTakeFocusOnClick === undefined ? false : props.disableTakeFocusOnClick;
@@ -4,7 +4,11 @@ import { useLexicalComposerContext } from './LexicalComposerContext';
4
4
  import { registerClearEditor } from '@lexical/extension';
5
5
  import { useLayoutEffect } from 'octane';
6
6
 
7
- export function ClearEditorPlugin(props) {
7
+ type Props = Readonly<{
8
+ onClear?: () => void;
9
+ }>;
10
+
11
+ export function ClearEditorPlugin(props: Props) {
8
12
  const [editor] = useLexicalComposerContext();
9
13
  const onClear = props.onClear;
10
14
  useLayoutEffect(() => registerClearEditor(editor, onClear), [editor, onClear]);
@@ -4,7 +4,12 @@ import { registerClickableLink } from '@lexical/link';
4
4
  import { useLexicalComposerContext } from './LexicalComposerContext';
5
5
  import { useEffect } from 'octane';
6
6
 
7
- export function ClickableLinkPlugin(props) {
7
+ type Props = {
8
+ newTab?: boolean;
9
+ disabled?: boolean;
10
+ };
11
+
12
+ export function ClickableLinkPlugin(props: Props) {
8
13
  const [editor] = useLexicalComposerContext();
9
14
  const newTab = props.newTab !== false;
10
15
  const disabled = props.disabled === true;
@@ -2,6 +2,9 @@
2
2
  // initialConfig once (useMemo), seeds its initial state, provides it via context,
3
3
  // and renders children. `useLayoutEffect` applies the editable flag on mount.
4
4
  import { createLexicalComposerContext, LexicalComposerContext } from './LexicalComposerContext';
5
+ import type { LexicalComposerContextWithEditor } from './LexicalComposerContext';
6
+ import type { InitialConfigType, InitialEditorStateType } from './types';
7
+ import type { LexicalEditor } from 'lexical';
5
8
  import {
6
9
  $createParagraphNode,
7
10
  $getRoot,
@@ -11,11 +14,12 @@ import {
11
14
  getActiveElement,
12
15
  HISTORY_MERGE_TAG,
13
16
  } from 'lexical';
17
+ import type { OctaneNode } from 'octane';
14
18
  import { useMemo, useLayoutEffect } from 'octane';
15
19
 
16
20
  const HISTORY_MERGE_OPTIONS = { tag: HISTORY_MERGE_TAG };
17
21
 
18
- function initializeEditor(editor, initialEditorState) {
22
+ function initializeEditor(editor: LexicalEditor, initialEditorState?: InitialEditorStateType) {
19
23
  if (initialEditorState === null) {
20
24
  return;
21
25
  } else if (initialEditorState === undefined) {
@@ -58,10 +62,15 @@ function initializeEditor(editor, initialEditorState) {
58
62
  }
59
63
  }
60
64
 
61
- export function LexicalComposer(props) @{
65
+ type Props = {
66
+ initialConfig: InitialConfigType;
67
+ children?: OctaneNode;
68
+ };
69
+
70
+ export function LexicalComposer(props: Props) @{
62
71
  const initialConfig = props.initialConfig;
63
72
 
64
- const composerContext = useMemo(() => {
73
+ const composerContext = useMemo<LexicalComposerContextWithEditor>(() => {
65
74
  const {
66
75
  theme,
67
76
  namespace,
@@ -80,7 +89,7 @@ export function LexicalComposer(props) @{
80
89
  namespace,
81
90
  nodes,
82
91
  onError: (error) => onError(error, editor),
83
- ...(onWarn ? { onWarn: (error) => onWarn(error, editor) } : {}),
92
+ ...(onWarn ? { onWarn: (error: Error) => onWarn(error, editor) } : {}),
84
93
  theme,
85
94
  });
86
95
  initializeEditor(editor, initialEditorState);
@@ -2,15 +2,37 @@
2
2
  // context and renders the editable element; an optional `placeholder` is shown
3
3
  // while the editor can show one. React's `forwardRef` becomes a plain `ref` prop
4
4
  // (spread through to the element).
5
+ import type { LexicalEditor } from 'lexical';
5
6
  import { useLexicalComposerContext } from './LexicalComposerContext';
6
7
  import { useCanShowPlaceholder } from './shared/useCanShowPlaceholder';
7
8
  import { ContentEditableElement } from './shared/LexicalContentEditableElement.tsrx';
9
+ import type { ContentEditableElementProps } from './shared/LexicalContentEditableElement.tsrx';
8
10
  import { useLayoutEffect, useState } from 'octane';
11
+ import type { OctaneNode } from 'octane';
9
12
 
10
13
  // Re-export the lower-level element (matches @lexical/react/LexicalContentEditable).
11
14
  export { ContentEditableElement } from './shared/LexicalContentEditableElement.tsrx';
12
15
 
13
- function Placeholder(props) {
16
+ /**
17
+ * Props for the {@link ContentEditable} component. These are the
18
+ * {@link ContentEditableElementProps} (minus `editor`, which is read from
19
+ * context) plus an optional `placeholder`; when a `placeholder` is provided an
20
+ * `aria-placeholder` string is also required for accessibility.
21
+ */
22
+ export type ContentEditableProps =
23
+ Omit<ContentEditableElementProps, 'editor'> & (| {
24
+ 'aria-placeholder'?: void;
25
+ placeholder?: null;
26
+ }
27
+ | {
28
+ 'aria-placeholder': string;
29
+ placeholder: ((isEditable: boolean) => OctaneNode) | OctaneNode;
30
+ });
31
+
32
+ function Placeholder(props: {
33
+ editor: LexicalEditor;
34
+ content: ContentEditableProps['placeholder'];
35
+ }) {
14
36
  const editor = props.editor;
15
37
  const showPlaceholder = useCanShowPlaceholder(editor);
16
38
  const [isEditable, setEditable] = useState(editor.isEditable());
@@ -27,7 +49,7 @@ function Placeholder(props) {
27
49
  }
28
50
 
29
51
  const content = props.content;
30
- let placeholder = null;
52
+ let placeholder: OctaneNode = null;
31
53
  if (typeof content === 'function') {
32
54
  placeholder = content(isEditable);
33
55
  } else if (content !== null) {
@@ -39,7 +61,7 @@ function Placeholder(props) {
39
61
  return <div aria-hidden="true">{placeholder}</div>;
40
62
  }
41
63
 
42
- export function ContentEditable(props) @{
64
+ export function ContentEditable(props: ContentEditableProps) @{
43
65
  const [editor] = useLexicalComposerContext();
44
66
  <>
45
67
  <ContentEditableElement editor={editor} {...props} />
@@ -25,7 +25,9 @@ import {
25
25
  isHTMLElement,
26
26
  mergeRegister,
27
27
  } from 'lexical';
28
+ import type { LexicalEditor } from 'lexical';
28
29
  import { createPortal, useCallback, useEffect, useRef, useState } from 'octane';
30
+ import type { OctaneNode } from 'octane';
29
31
 
30
32
  import { Point } from './shared/point';
31
33
  import { Rectangle } from './shared/rect';
@@ -41,7 +43,7 @@ const Indeterminate = 0;
41
43
 
42
44
  let prevIndex = Infinity;
43
45
 
44
- function getCurrentIndex(keysLength) {
46
+ function getCurrentIndex(keysLength: number): number {
45
47
  if (keysLength === 0) {
46
48
  return Infinity;
47
49
  }
@@ -51,14 +53,16 @@ function getCurrentIndex(keysLength) {
51
53
  return Math.floor(keysLength / 2);
52
54
  }
53
55
 
54
- function getTopLevelNodeKeys(editor) {
56
+ function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
55
57
  return editor.read('latest', () => $getRoot().getChildrenKeys());
56
58
  }
57
59
 
58
- function getCollapsedMargins(elem) {
59
- const getMargin = (element, margin) => element
60
- ? parseFloat(window.getComputedStyle(element)[margin])
61
- : 0;
60
+ function getCollapsedMargins(elem: HTMLElement): {
61
+ marginTop: number;
62
+ marginBottom: number;
63
+ } {
64
+ const getMargin = (element: Element | null, margin: 'marginTop' | 'marginBottom'): number =>
65
+ element ? parseFloat(window.getComputedStyle(element)[margin]) : 0;
62
66
 
63
67
  const { marginTop, marginBottom } = window.getComputedStyle(elem);
64
68
  const prevElemSiblingMarginBottom = getMargin(elem.previousElementSibling, 'marginBottom');
@@ -69,11 +73,16 @@ function getCollapsedMargins(elem) {
69
73
  return { marginBottom: collapsedBottomMargin, marginTop: collapsedTopMargin };
70
74
  }
71
75
 
72
- function getBlockElement(anchorElem, editor, event, useEdgeAsDefault = false) {
76
+ function getBlockElement(
77
+ anchorElem: HTMLElement,
78
+ editor: LexicalEditor,
79
+ event: MouseEvent,
80
+ useEdgeAsDefault = false,
81
+ ): HTMLElement | null {
73
82
  const anchorElementRect = anchorElem.getBoundingClientRect();
74
83
  const topLevelNodeKeys = getTopLevelNodeKeys(editor);
75
84
 
76
- let blockElem = null;
85
+ let blockElem: HTMLElement | null = null;
77
86
 
78
87
  editor.read('latest', () => {
79
88
  if (useEdgeAsDefault) {
@@ -147,7 +156,12 @@ function getBlockElement(anchorElem, editor, event, useEdgeAsDefault = false) {
147
156
  return blockElem;
148
157
  }
149
158
 
150
- function setMenuPosition(targetElem, floatingElem, anchorElem, zoomLevel) {
159
+ function setMenuPosition(
160
+ targetElem: HTMLElement | null,
161
+ floatingElem: HTMLElement,
162
+ anchorElem: HTMLElement,
163
+ zoomLevel: number,
164
+ ) {
151
165
  if (!targetElem) {
152
166
  floatingElem.style.display = 'none';
153
167
  return;
@@ -176,7 +190,7 @@ function setMenuPosition(targetElem, floatingElem, anchorElem, zoomLevel) {
176
190
  floatingElem.style.transform = `translate(${left}px, ${top}px)`;
177
191
  }
178
192
 
179
- function setDragImage(dataTransfer, draggableBlockElem) {
193
+ function setDragImage(dataTransfer: DataTransfer, draggableBlockElem: HTMLElement) {
180
194
  const { transform } = draggableBlockElem.style;
181
195
 
182
196
  draggableBlockElem.style.transform = 'translateZ(0)';
@@ -187,7 +201,12 @@ function setDragImage(dataTransfer, draggableBlockElem) {
187
201
  });
188
202
  }
189
203
 
190
- function setTargetLine(targetLineElem, targetBlockElem, mouseY, anchorElem) {
204
+ function setTargetLine(
205
+ targetLineElem: HTMLElement,
206
+ targetBlockElem: HTMLElement,
207
+ mouseY: number,
208
+ anchorElem: HTMLElement,
209
+ ) {
191
210
  const { top: targetBlockElemTop, height: targetBlockElemHeight } =
192
211
  targetBlockElem.getBoundingClientRect();
193
212
  const { top: anchorTop, width: anchorWidth } = anchorElem.getBoundingClientRect();
@@ -207,14 +226,22 @@ function setTargetLine(targetLineElem, targetBlockElem, mouseY, anchorElem) {
207
226
  targetLineElem.style.opacity = '.4';
208
227
  }
209
228
 
210
- function hideTargetLine(targetLineElem) {
229
+ function hideTargetLine(targetLineElem: HTMLElement | null) {
211
230
  if (targetLineElem) {
212
231
  targetLineElem.style.opacity = '0';
213
232
  targetLineElem.style.transform = 'translate(-10000px, -10000px)';
214
233
  }
215
234
  }
216
235
 
217
- function DraggableContainer(props) @{
236
+ type DraggableContainerProps = {
237
+ onDragStart: (event: DragEvent) => void;
238
+ onDragEnd: () => void;
239
+ isEditable: boolean;
240
+ menuComponent: OctaneNode;
241
+ targetLineComponent: OctaneNode;
242
+ };
243
+
244
+ function DraggableContainer(props: DraggableContainerProps) @{
218
245
  <>
219
246
  <div draggable={true} onDragStart={props.onDragStart} onDragEnd={props.onDragEnd}>
220
247
  {props.isEditable ? props.menuComponent : null}
@@ -224,22 +251,22 @@ function DraggableContainer(props) @{
224
251
  }
225
252
 
226
253
  function useDraggableBlockMenu(
227
- editor,
228
- anchorElem,
229
- menuRef,
230
- targetLineRef,
231
- isEditable,
232
- menuComponent,
233
- targetLineComponent,
234
- isOnMenu,
235
- onElementChanged,
236
- ) {
254
+ editor: LexicalEditor,
255
+ anchorElem: HTMLElement,
256
+ menuRef: { current: HTMLElement | null },
257
+ targetLineRef: { current: HTMLElement | null },
258
+ isEditable: boolean,
259
+ menuComponent: OctaneNode,
260
+ targetLineComponent: OctaneNode,
261
+ isOnMenu: (element: HTMLElement) => boolean,
262
+ onElementChanged: ((element: HTMLElement | null) => void) | undefined,
263
+ ): OctaneNode {
237
264
  const scrollerElem = getParentElement(anchorElem);
238
265
 
239
266
  const isDraggingBlockRef = useRef(false);
240
- const [draggableBlockElem, setDraggableBlockElemState] = useState(null);
267
+ const [draggableBlockElem, setDraggableBlockElemState] = useState<HTMLElement | null>(null);
241
268
 
242
- const setDraggableBlockElem = useCallback((elem) => {
269
+ const setDraggableBlockElem = useCallback((elem: HTMLElement | null) => {
243
270
  setDraggableBlockElemState(elem);
244
271
  if (onElementChanged) {
245
272
  onElementChanged(elem);
@@ -247,7 +274,7 @@ function useDraggableBlockMenu(
247
274
  }, [onElementChanged]);
248
275
 
249
276
  useEffect(() => {
250
- function onMouseMove(event) {
277
+ function onMouseMove(event: MouseEvent) {
251
278
  const target = getComposedEventTarget(event);
252
279
  if (!isHTMLElement(target)) {
253
280
  setDraggableBlockElem(null);
@@ -287,7 +314,7 @@ function useDraggableBlockMenu(
287
314
  }, [editor, anchorElem, draggableBlockElem, menuRef]);
288
315
 
289
316
  useEffect(() => {
290
- function onDragover(event) {
317
+ function onDragover(event: DragEvent): boolean {
291
318
  if (!isDraggingBlockRef.current) {
292
319
  return false;
293
320
  }
@@ -315,7 +342,7 @@ function useDraggableBlockMenu(
315
342
  return true;
316
343
  }
317
344
 
318
- function $onDrop(event) {
345
+ function $onDrop(event: DragEvent): boolean {
319
346
  if (!isDraggingBlockRef.current) {
320
347
  return false;
321
348
  }
@@ -390,7 +417,7 @@ function useDraggableBlockMenu(
390
417
 
391
418
  return mergeRegister(
392
419
  editor.registerRootListener((rootElement) => {
393
- function onBlur(event) {
420
+ function onBlur(event: FocusEvent) {
394
421
  const relatedTarget = event.relatedTarget;
395
422
  if (isHTMLElement(relatedTarget) && isOnMenu(relatedTarget)) {
396
423
  if (rootElement) {
@@ -433,7 +460,7 @@ function useDraggableBlockMenu(
433
460
  );
434
461
  }, [editor, isEditable, isOnMenu]);
435
462
 
436
- function onDragStart(event) {
463
+ function onDragStart(event: DragEvent) {
437
464
  const dataTransfer = event.dataTransfer;
438
465
  if (!dataTransfer || !draggableBlockElem) {
439
466
  return;
@@ -482,7 +509,17 @@ function useDraggableBlockMenu(
482
509
  });
483
510
  }
484
511
 
485
- export function DraggableBlockPlugin_EXPERIMENTAL(props) {
512
+ type Props = {
513
+ anchorElem?: HTMLElement;
514
+ menuRef: { current: HTMLElement | null };
515
+ targetLineRef: { current: HTMLElement | null };
516
+ menuComponent: OctaneNode;
517
+ targetLineComponent: OctaneNode;
518
+ isOnMenu: (element: HTMLElement) => boolean;
519
+ onElementChanged?: (element: HTMLElement | null) => void;
520
+ };
521
+
522
+ export function DraggableBlockPlugin_EXPERIMENTAL(props: Props) {
486
523
  const anchorElem = props.anchorElem ?? document.body;
487
524
  const [editor] = useLexicalComposerContext();
488
525
  return useDraggableBlockMenu(
@@ -1,9 +1,15 @@
1
1
  // Ported from @lexical/react/src/LexicalEditorRefPlugin.tsx. Hands the editor to
2
2
  // a callback ref or object ref so UI outside the composer can reach it.
3
3
  import { useLexicalComposerContext } from './LexicalComposerContext';
4
+ import type { LexicalEditor } from 'lexical';
4
5
  import { useEffect } from 'octane';
5
6
 
6
- export function EditorRefPlugin(props) {
7
+ type Props = {
8
+ editorRef: | ((editor: LexicalEditor | null) => void)
9
+ | { current: LexicalEditor | null | undefined };
10
+ };
11
+
12
+ export function EditorRefPlugin(props: Props) {
7
13
  const [editor] = useLexicalComposerContext();
8
14
  const editorRef = props.editorRef;
9
15
 
@@ -4,10 +4,26 @@
4
4
  // the caught error) — preserving the (error, info) callback Rich/PlainTextPlugin
5
5
  // pass for decorator-render failures.
6
6
  import { ErrorBoundary } from 'octane';
7
+ import type { OctaneNode } from 'octane';
7
8
 
8
- export function LexicalErrorBoundary(props) @{
9
+ // Octane analog of React's `ErrorInfo`.
10
+ type ErrorInfo = {
11
+ componentStack?: string | null;
12
+ digest?: string | null;
13
+ };
14
+
15
+ /**
16
+ * Props for the {@link LexicalErrorBoundary} component.
17
+ */
18
+ export type LexicalErrorBoundaryProps = {
19
+ children: OctaneNode;
20
+ fallback?: OctaneNode;
21
+ onError: (error: Error, info: ErrorInfo) => void;
22
+ };
23
+
24
+ export function LexicalErrorBoundary(props: LexicalErrorBoundaryProps) @{
9
25
  <ErrorBoundary
10
- fallback={(error, _reset) => {
26
+ fallback={(error: unknown, _reset: VoidFunction) => {
11
27
  if (props.onError) {
12
28
  props.onError(error instanceof Error ? error : new Error(String(error), { cause: error }), {
13
29
  componentStack: '',
@@ -3,11 +3,17 @@
3
3
  // useMemo/useEffect cover it directly).
4
4
  import { useLexicalComposerContext } from './LexicalComposerContext';
5
5
  import { createEmptyHistoryState, registerHistory } from '@lexical/history';
6
+ import type { HistoryState } from '@lexical/history';
6
7
  import { useEffect, useMemo } from 'octane';
7
8
 
8
9
  export { createEmptyHistoryState } from '@lexical/history';
9
10
 
10
- export function HistoryPlugin(props) {
11
+ type Props = {
12
+ delay?: number;
13
+ externalHistoryState?: HistoryState;
14
+ };
15
+
16
+ export function HistoryPlugin(props: Props) {
11
17
  const [editor] = useLexicalComposerContext();
12
18
  const externalHistoryState = props.externalHistoryState;
13
19
  const delay =