@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.
- package/package.json +5 -5
- package/src/LexicalAutoEmbedPlugin.tsrx +31 -6
- package/src/LexicalAutoFocusPlugin.tsrx +5 -1
- package/src/LexicalAutoLinkPlugin.tsrx +9 -1
- package/src/LexicalBlockWithAlignableContents.tsrx +13 -1
- package/src/LexicalCharacterLimitPlugin.tsrx +18 -7
- package/src/LexicalCheckListPlugin.tsrx +5 -1
- package/src/LexicalClearEditorPlugin.tsrx +5 -1
- package/src/LexicalClickableLinkPlugin.tsrx +6 -1
- package/src/LexicalComposer.tsrx +13 -4
- package/src/LexicalContentEditable.tsrx +25 -3
- package/src/LexicalDraggableBlockPlugin.tsrx +68 -31
- package/src/LexicalEditorRefPlugin.tsrx +7 -1
- package/src/LexicalErrorBoundary.tsrx +18 -2
- package/src/LexicalHistoryPlugin.tsrx +7 -1
- package/src/LexicalHorizontalRuleNode.tsrx +10 -7
- package/src/LexicalLinkPlugin.tsrx +7 -1
- package/src/LexicalListPlugin.tsrx +17 -1
- package/src/LexicalMarkdownShortcutPlugin.tsrx +8 -3
- package/src/LexicalNestedComposer.tsrx +21 -3
- package/src/LexicalNodeContextMenuPlugin.tsrx +76 -22
- package/src/LexicalNodeEventPlugin.tsrx +10 -3
- package/src/LexicalNodeMenuPlugin.tsrx +21 -3
- package/src/LexicalOnChangePlugin.tsrx +8 -1
- package/src/LexicalPlainTextPlugin.tsrx +16 -2
- package/src/LexicalRichTextPlugin.tsrx +16 -2
- package/src/LexicalSelectionAlwaysOnDisplay.tsrx +5 -1
- package/src/LexicalTabIndentationPlugin.tsrx +12 -1
- package/src/LexicalTableOfContentsPlugin.tsrx +39 -12
- package/src/LexicalTablePlugin.tsrx +34 -1
- package/src/LexicalTypeaheadMenuPlugin.tsrx +24 -3
- package/src/shared/LexicalContentEditableElement.tsrx +58 -10
- package/src/shared/LexicalDecorators.tsrx +28 -5
- 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.
|
|
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.
|
|
108
|
+
"@octanejs/floating-ui": "0.1.10"
|
|
109
109
|
},
|
|
110
110
|
"peerDependencies": {
|
|
111
|
-
"octane": "0.1.
|
|
111
|
+
"octane": "0.1.11"
|
|
112
112
|
},
|
|
113
113
|
"devDependencies": {
|
|
114
114
|
"@lexical/react": "0.46.0",
|
|
115
|
-
"@tsrx/react": "^0.2.
|
|
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.
|
|
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
|
-
|
|
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 = (
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/LexicalComposer.tsrx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|