@octanejs/lexical 0.1.8 → 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
|
@@ -3,15 +3,32 @@
|
|
|
3
3
|
// component: `children(entries, editor)` returns the element to render.
|
|
4
4
|
import { useLexicalComposerContext } from './LexicalComposerContext';
|
|
5
5
|
import { $isHeadingNode, HeadingNode } from '@lexical/rich-text';
|
|
6
|
+
import type { HeadingTagType } from '@lexical/rich-text';
|
|
6
7
|
import { $getNextRightPreorderNode } from '@lexical/utils';
|
|
7
8
|
import { $getNodeByKey, $getRoot, $isElementNode, TextNode } from 'lexical';
|
|
9
|
+
import type { ElementNode, LexicalEditor, LexicalNode, NodeKey } from 'lexical';
|
|
8
10
|
import { useEffect, useState } from 'octane';
|
|
11
|
+
import type { OctaneNode } from 'octane';
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
/**
|
|
14
|
+
* A single entry in the table of contents, as a tuple of the heading node's
|
|
15
|
+
* {@link NodeKey}, its text content, and its heading tag (for example `'h1'`).
|
|
16
|
+
*/
|
|
17
|
+
export type TableOfContentsEntry = [key: NodeKey, text: string, tag: HeadingTagType];
|
|
18
|
+
|
|
19
|
+
type Props = {
|
|
20
|
+
children: (values: Array<TableOfContentsEntry>, editor: LexicalEditor) => OctaneNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function toEntry(heading: HeadingNode): TableOfContentsEntry {
|
|
11
24
|
return [heading.getKey(), heading.getTextContent(), heading.getTag()];
|
|
12
25
|
}
|
|
13
26
|
|
|
14
|
-
function $insertHeadingIntoTableOfContents(
|
|
27
|
+
function $insertHeadingIntoTableOfContents(
|
|
28
|
+
prevHeading: HeadingNode | null,
|
|
29
|
+
newHeading: HeadingNode | null,
|
|
30
|
+
currentTableOfContents: Array<TableOfContentsEntry>,
|
|
31
|
+
): Array<TableOfContentsEntry> {
|
|
15
32
|
if (newHeading === null) {
|
|
16
33
|
return currentTableOfContents;
|
|
17
34
|
}
|
|
@@ -40,7 +57,10 @@ function $insertHeadingIntoTableOfContents(prevHeading, newHeading, currentTable
|
|
|
40
57
|
return newTableOfContents;
|
|
41
58
|
}
|
|
42
59
|
|
|
43
|
-
function $deleteHeadingFromTableOfContents(
|
|
60
|
+
function $deleteHeadingFromTableOfContents(
|
|
61
|
+
key: NodeKey,
|
|
62
|
+
currentTableOfContents: Array<TableOfContentsEntry>,
|
|
63
|
+
): Array<TableOfContentsEntry> {
|
|
44
64
|
const newTableOfContents = [];
|
|
45
65
|
for (const heading of currentTableOfContents) {
|
|
46
66
|
if (heading[0] !== key) {
|
|
@@ -50,7 +70,10 @@ function $deleteHeadingFromTableOfContents(key, currentTableOfContents) {
|
|
|
50
70
|
return newTableOfContents;
|
|
51
71
|
}
|
|
52
72
|
|
|
53
|
-
function $updateHeadingInTableOfContents(
|
|
73
|
+
function $updateHeadingInTableOfContents(
|
|
74
|
+
heading: HeadingNode,
|
|
75
|
+
currentTableOfContents: Array<TableOfContentsEntry>,
|
|
76
|
+
): Array<TableOfContentsEntry> {
|
|
54
77
|
const newTableOfContents = [];
|
|
55
78
|
for (const oldHeading of currentTableOfContents) {
|
|
56
79
|
if (oldHeading[0] === heading.getKey()) {
|
|
@@ -62,8 +85,12 @@ function $updateHeadingInTableOfContents(heading, currentTableOfContents) {
|
|
|
62
85
|
return newTableOfContents;
|
|
63
86
|
}
|
|
64
87
|
|
|
65
|
-
function $updateHeadingPosition(
|
|
66
|
-
|
|
88
|
+
function $updateHeadingPosition(
|
|
89
|
+
prevHeading: HeadingNode | null,
|
|
90
|
+
heading: HeadingNode,
|
|
91
|
+
currentTableOfContents: Array<TableOfContentsEntry>,
|
|
92
|
+
): Array<TableOfContentsEntry> {
|
|
93
|
+
const newTableOfContents: Array<TableOfContentsEntry> = [];
|
|
67
94
|
const newEntry = toEntry(heading);
|
|
68
95
|
|
|
69
96
|
if (!prevHeading) {
|
|
@@ -82,7 +109,7 @@ function $updateHeadingPosition(prevHeading, heading, currentTableOfContents) {
|
|
|
82
109
|
return newTableOfContents;
|
|
83
110
|
}
|
|
84
111
|
|
|
85
|
-
function $getPreviousHeading(node) {
|
|
112
|
+
function $getPreviousHeading(node: LexicalNode): HeadingNode | null {
|
|
86
113
|
let prevHeading = $getNextRightPreorderNode(node);
|
|
87
114
|
while (prevHeading !== null && !$isHeadingNode(prevHeading)) {
|
|
88
115
|
prevHeading = $getNextRightPreorderNode(prevHeading);
|
|
@@ -90,14 +117,14 @@ function $getPreviousHeading(node) {
|
|
|
90
117
|
return prevHeading;
|
|
91
118
|
}
|
|
92
119
|
|
|
93
|
-
export function TableOfContentsPlugin(props) {
|
|
94
|
-
const [tableOfContents, setTableOfContents] = useState([]);
|
|
120
|
+
export function TableOfContentsPlugin(props: Props) {
|
|
121
|
+
const [tableOfContents, setTableOfContents] = useState<Array<TableOfContentsEntry>>([]);
|
|
95
122
|
const [editor] = useLexicalComposerContext();
|
|
96
123
|
|
|
97
124
|
useEffect(() => {
|
|
98
|
-
let currentTableOfContents = [];
|
|
125
|
+
let currentTableOfContents: Array<TableOfContentsEntry> = [];
|
|
99
126
|
editor.read('latest', () => {
|
|
100
|
-
const updateCurrentTableOfContents = (node) => {
|
|
127
|
+
const updateCurrentTableOfContents = (node: ElementNode) => {
|
|
101
128
|
for (const child of node.getChildren()) {
|
|
102
129
|
if ($isHeadingNode(child)) {
|
|
103
130
|
currentTableOfContents.push([
|
|
@@ -120,7 +147,7 @@ export function TableOfContentsPlugin(props) {
|
|
|
120
147
|
dirtyElements,
|
|
121
148
|
}) => {
|
|
122
149
|
editorState.read(() => {
|
|
123
|
-
const updateChildHeadings = (node) => {
|
|
150
|
+
const updateChildHeadings = (node: ElementNode) => {
|
|
124
151
|
for (const child of node.getChildren()) {
|
|
125
152
|
if ($isHeadingNode(child)) {
|
|
126
153
|
const prevHeading = $getPreviousHeading(child);
|
|
@@ -12,7 +12,40 @@ import { $fullReconcile } from 'lexical';
|
|
|
12
12
|
import { useLexicalComposerContext } from './LexicalComposerContext';
|
|
13
13
|
import { useEffect, useState } from 'octane';
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Props for the {@link TablePlugin} component.
|
|
17
|
+
*/
|
|
18
|
+
export interface TablePluginProps {
|
|
19
|
+
/**
|
|
20
|
+
* When `false` (default `true`), merged cell support (colspan and rowspan)
|
|
21
|
+
* will be disabled and all tables will be forced into a regular grid with
|
|
22
|
+
* 1x1 table cells.
|
|
23
|
+
*/
|
|
24
|
+
hasCellMerge?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* When `false` (default `true`), the background color of TableCellNode will
|
|
27
|
+
* always be removed.
|
|
28
|
+
*/
|
|
29
|
+
hasCellBackgroundColor?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* When `true` (default `true`), the tab key can be used to navigate table
|
|
32
|
+
* cells.
|
|
33
|
+
*/
|
|
34
|
+
hasTabHandler?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* When `true` (default `false`), tables will be wrapped in a `<div>` to
|
|
37
|
+
* enable horizontal scrolling.
|
|
38
|
+
*/
|
|
39
|
+
hasHorizontalScroll?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* When `true` (default `false`), nested tables will be allowed.
|
|
42
|
+
*
|
|
43
|
+
* @experimental Nested tables are not officially supported.
|
|
44
|
+
*/
|
|
45
|
+
hasNestedTables?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function TablePlugin(props: TablePluginProps) {
|
|
16
49
|
const [editor] = useLexicalComposerContext();
|
|
17
50
|
const hasCellMerge =
|
|
18
51
|
props.hasCellMerge === undefined ? true : props.hasCellMerge;
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
tryToPositionRange,
|
|
12
12
|
} from './typeaheadShared';
|
|
13
13
|
import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW } from 'lexical';
|
|
14
|
+
import type { CommandListenerPriority, TextNode } from 'lexical';
|
|
14
15
|
import { startTransition, useCallback, useEffect, useState } from 'octane';
|
|
16
|
+
import type { MenuOption, MenuRenderFn, MenuResolution, TriggerFn } from './shared/menuShared';
|
|
15
17
|
|
|
16
18
|
export { useBasicTypeaheadTriggerMatch, PUNCTUATION } from './typeaheadShared';
|
|
17
19
|
export { MenuOption, SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND } from './shared/menuShared';
|
|
@@ -19,7 +21,26 @@ export { useDynamicPositioning } from './shared/useDynamicPositioning';
|
|
|
19
21
|
// Upstream re-exports @lexical/utils' getScrollParent from this module.
|
|
20
22
|
export { getScrollParent } from '@lexical/utils';
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Props for the {@link LexicalTypeaheadMenuPlugin} component.
|
|
26
|
+
*/
|
|
27
|
+
export type TypeaheadMenuPluginProps<TOption extends MenuOption> = {
|
|
28
|
+
onQueryChange: (matchingString: string | null) => void;
|
|
29
|
+
onSelectOption: (option: TOption, textNodeContainingQuery: | TextNode
|
|
30
|
+
| null, closeMenu: () => void, matchingString: string) => void;
|
|
31
|
+
options: TOption[];
|
|
32
|
+
triggerFn: TriggerFn;
|
|
33
|
+
menuRenderFn?: MenuRenderFn<TOption>;
|
|
34
|
+
onOpen?: (resolution: MenuResolution) => void;
|
|
35
|
+
onClose?: () => void | PromiseLike<void>;
|
|
36
|
+
anchorClassName?: string;
|
|
37
|
+
commandPriority?: CommandListenerPriority;
|
|
38
|
+
parent?: HTMLElement;
|
|
39
|
+
preselectFirstItem?: boolean;
|
|
40
|
+
ignoreEntityBoundary?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function LexicalTypeaheadMenuPlugin<TOption extends MenuOption>(props: TypeaheadMenuPluginProps<TOption>) {
|
|
23
44
|
const options = props.options;
|
|
24
45
|
const onQueryChange = props.onQueryChange;
|
|
25
46
|
const onSelectOption = props.onSelectOption;
|
|
@@ -35,7 +56,7 @@ export function LexicalTypeaheadMenuPlugin(props) {
|
|
|
35
56
|
const ignoreEntityBoundary = props.ignoreEntityBoundary === true;
|
|
36
57
|
|
|
37
58
|
const [editor] = useLexicalComposerContext();
|
|
38
|
-
const [resolution, setResolution] = useState(null);
|
|
59
|
+
const [resolution, setResolution] = useState<MenuResolution | null>(null);
|
|
39
60
|
const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName, parent);
|
|
40
61
|
|
|
41
62
|
const closeTypeahead = useCallback(() => {
|
|
@@ -55,7 +76,7 @@ export function LexicalTypeaheadMenuPlugin(props) {
|
|
|
55
76
|
}
|
|
56
77
|
}, [onClose, resolution]);
|
|
57
78
|
|
|
58
|
-
const openTypeahead = useCallback((res) => {
|
|
79
|
+
const openTypeahead = useCallback((res: MenuResolution) => {
|
|
59
80
|
setResolution(res);
|
|
60
81
|
if (onOpen != null && resolution === null) {
|
|
61
82
|
onOpen(res);
|
|
@@ -7,7 +7,42 @@
|
|
|
7
7
|
// The full ARIA set + `{...rest}` passthrough mirror @lexical/react exactly:
|
|
8
8
|
// boolean ARIA values are stringified (octane drops `false` as an attribute), and
|
|
9
9
|
// unknown props flow through via `rest` (everything not consumed by name).
|
|
10
|
+
import type { LexicalEditor } from 'lexical';
|
|
10
11
|
import { useCallback, useLayoutEffect, useState } from 'octane';
|
|
12
|
+
import type { Octane } from 'octane/jsx-runtime';
|
|
13
|
+
|
|
14
|
+
type DivAttributes = Octane.HTMLAttributes<HTMLDivElement>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Props for the {@link ContentEditableElement} component. In addition to an
|
|
18
|
+
* `editor`, it accepts the standard `<div>` attributes (except `placeholder`),
|
|
19
|
+
* including the hyphenated `aria-*` attributes, which are the preferred way to
|
|
20
|
+
* set ARIA properties. The camelCase `aria*` props (such as `ariaLabel`) are
|
|
21
|
+
* also accepted but are retained only for backwards compatibility.
|
|
22
|
+
*/
|
|
23
|
+
export type ContentEditableElementProps = {
|
|
24
|
+
editor: LexicalEditor;
|
|
25
|
+
ariaActiveDescendant?: DivAttributes['aria-activedescendant'];
|
|
26
|
+
ariaAutoComplete?: DivAttributes['aria-autocomplete'];
|
|
27
|
+
ariaControls?: DivAttributes['aria-controls'];
|
|
28
|
+
ariaDescribedBy?: DivAttributes['aria-describedby'];
|
|
29
|
+
ariaErrorMessage?: DivAttributes['aria-errormessage'];
|
|
30
|
+
ariaExpanded?: DivAttributes['aria-expanded'];
|
|
31
|
+
ariaInvalid?: DivAttributes['aria-invalid'];
|
|
32
|
+
ariaLabel?: DivAttributes['aria-label'];
|
|
33
|
+
ariaLabelledBy?: DivAttributes['aria-labelledby'];
|
|
34
|
+
ariaMultiline?: DivAttributes['aria-multiline'];
|
|
35
|
+
ariaOwns?: DivAttributes['aria-owns'];
|
|
36
|
+
ariaRequired?: DivAttributes['aria-required'];
|
|
37
|
+
autoCapitalize?: HTMLDivElement['autocapitalize'];
|
|
38
|
+
'data-testid'?: string | null | undefined;
|
|
39
|
+
// Octane ref shapes (no forwardRef): callback or ref object.
|
|
40
|
+
ref?: ((el: HTMLDivElement | null) => void) | { current: HTMLDivElement | null };
|
|
41
|
+
// Consumed (excluded from the `rest` passthrough) but never rendered —
|
|
42
|
+
// upstream omits `placeholder` from the accepted div attributes; the octane
|
|
43
|
+
// port receives it via `<ContentEditable {...props}>`'s spread instead.
|
|
44
|
+
placeholder?: unknown;
|
|
45
|
+
} & Omit<Octane.JSX.IntrinsicElements['div'], 'ref' | 'placeholder'>;
|
|
11
46
|
|
|
12
47
|
// Props consumed by name — everything else flows through to the <div> via `rest`.
|
|
13
48
|
const NAMED_PROPS = new Set([
|
|
@@ -37,11 +72,11 @@ const NAMED_PROPS = new Set([
|
|
|
37
72
|
'data-testid',
|
|
38
73
|
]);
|
|
39
74
|
|
|
40
|
-
export function ContentEditableElement(props) @{
|
|
75
|
+
export function ContentEditableElement(props: ContentEditableElementProps) @{
|
|
41
76
|
const editor = props.editor;
|
|
42
77
|
const [isEditable, setEditable] = useState(editor.isEditable());
|
|
43
78
|
|
|
44
|
-
const handleRef = useCallback((rootElement) => {
|
|
79
|
+
const handleRef = useCallback((rootElement: HTMLDivElement | null) => {
|
|
45
80
|
// defaultView is required for a root element. In multi-window setups it may
|
|
46
81
|
// not exist at certain points.
|
|
47
82
|
if (rootElement && rootElement.ownerDocument && rootElement.ownerDocument.defaultView) {
|
|
@@ -63,7 +98,7 @@ export function ContentEditableElement(props) @{
|
|
|
63
98
|
const rest: Record<string, any> = {};
|
|
64
99
|
for (const k in props) {
|
|
65
100
|
if (!NAMED_PROPS.has(k)) {
|
|
66
|
-
rest[k] = props[k];
|
|
101
|
+
rest[k] = (props as Record<string, unknown>)[k];
|
|
67
102
|
}
|
|
68
103
|
}
|
|
69
104
|
// Route `class` through the spread (setSpread omits an absent/undefined key) so
|
|
@@ -73,29 +108,42 @@ export function ContentEditableElement(props) @{
|
|
|
73
108
|
rest['class'] = props.className;
|
|
74
109
|
}
|
|
75
110
|
|
|
111
|
+
// Attribute names are authored camelCase (matching upstream and octane's typed
|
|
112
|
+
// JSX contract); the runtime writes the equivalent lowercase DOM attributes
|
|
113
|
+
// (`tabIndex` via ATTRIBUTE_ALIASES, the rest via HTML's case-insensitive
|
|
114
|
+
// setAttribute).
|
|
76
115
|
<div
|
|
77
|
-
ref={props.ref != null
|
|
116
|
+
ref={props.ref != null
|
|
117
|
+
? // Octane's runtime accepts an array of refs natively; the checker's
|
|
118
|
+
// merged-ref signature only models the single-ref forms, so the
|
|
119
|
+
// dynamic array branch needs a type-level cast.
|
|
120
|
+
[props.ref, handleRef] as unknown as typeof handleRef
|
|
121
|
+
: handleRef}
|
|
78
122
|
aria-activedescendant={isEditable ? props.ariaActiveDescendant : undefined}
|
|
79
123
|
aria-autocomplete={isEditable ? props.ariaAutoComplete : 'none'}
|
|
80
124
|
aria-controls={isEditable ? props.ariaControls : undefined}
|
|
81
125
|
aria-describedby={props.ariaDescribedBy}
|
|
82
126
|
aria-errormessage={props.ariaErrorMessage != null ? props.ariaErrorMessage : undefined}
|
|
83
|
-
aria-expanded={isEditable && role === 'combobox'
|
|
127
|
+
aria-expanded={isEditable && role === 'combobox'
|
|
128
|
+
? String(!!props.ariaExpanded) as 'true' | 'false'
|
|
129
|
+
: undefined}
|
|
84
130
|
aria-invalid={props.ariaInvalid != null ? props.ariaInvalid : undefined}
|
|
85
131
|
aria-label={props.ariaLabel}
|
|
86
132
|
aria-labelledby={props.ariaLabelledBy}
|
|
87
133
|
aria-multiline={props.ariaMultiline}
|
|
88
134
|
aria-owns={isEditable ? props.ariaOwns : undefined}
|
|
89
135
|
aria-readonly={isEditable ? undefined : 'true'}
|
|
90
|
-
aria-required={props.ariaRequired != null
|
|
91
|
-
|
|
92
|
-
|
|
136
|
+
aria-required={props.ariaRequired != null
|
|
137
|
+
? String(props.ariaRequired) as 'true' | 'false'
|
|
138
|
+
: undefined}
|
|
139
|
+
autoCapitalize={props.autoCapitalize}
|
|
140
|
+
contentEditable={isEditable ? 'true' : 'false'}
|
|
93
141
|
data-testid={props['data-testid']}
|
|
94
142
|
id={props.id}
|
|
95
143
|
role={role}
|
|
96
|
-
|
|
144
|
+
spellCheck={props.spellCheck === false ? 'false' : 'true'}
|
|
97
145
|
style={props.style}
|
|
98
|
-
|
|
146
|
+
tabIndex={props.tabIndex}
|
|
99
147
|
{...rest}
|
|
100
148
|
/>
|
|
101
149
|
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// element (a createPortal VALUE is inert), so each portal lives inside its own
|
|
9
9
|
// `DecoratorPortal` (a display:contents wrapper — the content escapes into the
|
|
10
10
|
// node's element).
|
|
11
|
+
import type { LexicalEditor } from 'lexical';
|
|
12
|
+
import type { OctaneNode } from 'octane';
|
|
11
13
|
import {
|
|
12
14
|
useState,
|
|
13
15
|
useEffect,
|
|
@@ -19,18 +21,39 @@ import {
|
|
|
19
21
|
Suspense,
|
|
20
22
|
} from 'octane';
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
type ErrorBoundaryType = (props: {
|
|
25
|
+
children: OctaneNode;
|
|
26
|
+
onError: (error: Error) => void;
|
|
27
|
+
}) => OctaneNode;
|
|
28
|
+
|
|
29
|
+
type DecoratorPortalBodyProps = {
|
|
30
|
+
ErrorBoundary: ErrorBoundaryType;
|
|
31
|
+
onError: (error: Error) => void;
|
|
32
|
+
decorator: OctaneNode;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function DecoratorPortalBody(props: DecoratorPortalBodyProps) {
|
|
23
36
|
return createElement(props.ErrorBoundary, {
|
|
24
37
|
onError: props.onError,
|
|
25
38
|
children: createElement(Suspense, { fallback: null, children: props.decorator }),
|
|
26
39
|
});
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
type DecoratorPortalProps = {
|
|
43
|
+
element: HTMLElement;
|
|
44
|
+
bodyProps: DecoratorPortalBodyProps;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function DecoratorPortal(props: DecoratorPortalProps) {
|
|
30
48
|
return createPortal(DecoratorPortalBody, props.element, props.bodyProps);
|
|
31
49
|
}
|
|
32
50
|
|
|
33
|
-
|
|
51
|
+
type DecoratorsProps = {
|
|
52
|
+
editor: LexicalEditor;
|
|
53
|
+
ErrorBoundary: ErrorBoundaryType;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function Decorators(props: DecoratorsProps) {
|
|
34
57
|
const editor = props.editor;
|
|
35
58
|
const ErrorBoundary = props.ErrorBoundary;
|
|
36
59
|
|
|
@@ -38,7 +61,7 @@ export function Decorators(props) {
|
|
|
38
61
|
|
|
39
62
|
// Subscribe to decorator changes.
|
|
40
63
|
useLayoutEffect(() => {
|
|
41
|
-
return editor.registerDecoratorListener((nextDecorators) => {
|
|
64
|
+
return editor.registerDecoratorListener((nextDecorators: Record<string, OctaneNode>) => {
|
|
42
65
|
flushSync(() => {
|
|
43
66
|
setDecorators(nextDecorators);
|
|
44
67
|
});
|
|
@@ -79,7 +102,7 @@ export function Decorators(props) {
|
|
|
79
102
|
element={entry.element}
|
|
80
103
|
bodyProps={{
|
|
81
104
|
ErrorBoundary,
|
|
82
|
-
onError: (e) => editor._onError(e),
|
|
105
|
+
onError: (e: Error) => editor._onError(e),
|
|
83
106
|
decorator: entry.decorator,
|
|
84
107
|
}}
|
|
85
108
|
/>,
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// @lexical/react/src/shared/LexicalMenu.tsx. A `.tsrx` component (auto-slotted
|
|
3
3
|
// hooks). The hooks (useMenuAnchorRef / useDynamicPositioning) + non-component
|
|
4
4
|
// pieces live in the sibling `.ts` files.
|
|
5
|
+
import type { MenuOption, MenuRef, MenuRenderFn, MenuResolution } from './menuShared';
|
|
5
6
|
import {
|
|
6
7
|
$splitNodeContainingQuery,
|
|
7
8
|
scrollIntoViewIfNeeded,
|
|
8
9
|
SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND,
|
|
9
10
|
} from './menuShared';
|
|
11
|
+
import type { CommandListenerPriority, LexicalEditor, TextNode } from 'lexical';
|
|
10
12
|
import {
|
|
11
13
|
COMMAND_PRIORITY_LOW,
|
|
12
14
|
KEY_ARROW_DOWN_COMMAND,
|
|
@@ -18,9 +20,17 @@ import {
|
|
|
18
20
|
} from 'lexical';
|
|
19
21
|
import { createPortal, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'octane';
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
type MenuItemProps<TOption extends MenuOption> = {
|
|
24
|
+
index: number;
|
|
25
|
+
isSelected: boolean;
|
|
26
|
+
onClick: () => void;
|
|
27
|
+
onMouseEnter: () => void;
|
|
28
|
+
option: TOption;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function MenuItem<TOption extends MenuOption>(props: MenuItemProps<TOption>) @{
|
|
22
32
|
<li
|
|
23
|
-
|
|
33
|
+
tabIndex={-1}
|
|
24
34
|
class={props.isSelected ? 'item selected' : 'item'}
|
|
25
35
|
ref={props.option.setRefElement}
|
|
26
36
|
role="option"
|
|
@@ -34,7 +44,14 @@ function MenuItem(props) @{
|
|
|
34
44
|
</li>
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
type DefaultMenuProps<TOption extends MenuOption> = {
|
|
48
|
+
options: TOption[];
|
|
49
|
+
selectedIndex: number | null;
|
|
50
|
+
selectOptionAndCleanUp: (option: TOption) => void;
|
|
51
|
+
setHighlightedIndex: (index: number) => void;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function DefaultMenu<TOption extends MenuOption>(props: DefaultMenuProps<TOption>) @{
|
|
38
55
|
<div class="typeahead-popover mentions-menu">
|
|
39
56
|
<ul>{props.options.map(
|
|
40
57
|
(option, i) => <MenuItem
|
|
@@ -52,7 +69,21 @@ function DefaultMenu(props) @{
|
|
|
52
69
|
</div>
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
type LexicalMenuProps<TOption extends MenuOption> = {
|
|
73
|
+
close: () => void;
|
|
74
|
+
editor: LexicalEditor;
|
|
75
|
+
anchorElementRef: MenuRef;
|
|
76
|
+
resolution: MenuResolution;
|
|
77
|
+
options: TOption[];
|
|
78
|
+
shouldSplitNodeWithQuery?: boolean;
|
|
79
|
+
menuRenderFn?: MenuRenderFn<TOption>;
|
|
80
|
+
onSelectOption: (option: TOption, textNodeContainingQuery: | TextNode
|
|
81
|
+
| null, closeMenu: () => void, matchingString: string) => void;
|
|
82
|
+
commandPriority?: CommandListenerPriority;
|
|
83
|
+
preselectFirstItem?: boolean;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export function LexicalMenu<TOption extends MenuOption>(props: LexicalMenuProps<TOption>) {
|
|
56
87
|
const close = props.close;
|
|
57
88
|
const editor = props.editor;
|
|
58
89
|
const anchorElementRef = props.anchorElementRef;
|
|
@@ -65,7 +96,7 @@ export function LexicalMenu(props) {
|
|
|
65
96
|
props.commandPriority != null ? props.commandPriority : COMMAND_PRIORITY_LOW;
|
|
66
97
|
const preselectFirstItem = props.preselectFirstItem !== false;
|
|
67
98
|
|
|
68
|
-
const [rawSelectedIndex, setHighlightedIndex] = useState(null);
|
|
99
|
+
const [rawSelectedIndex, setHighlightedIndex] = useState<number | null>(null);
|
|
69
100
|
const selectedIndex =
|
|
70
101
|
rawSelectedIndex !== null ? Math.min(options.length - 1, rawSelectedIndex) : null;
|
|
71
102
|
|
|
@@ -77,7 +108,7 @@ export function LexicalMenu(props) {
|
|
|
77
108
|
}
|
|
78
109
|
}, [matchingString, preselectFirstItem]);
|
|
79
110
|
|
|
80
|
-
const selectOptionAndCleanUp = useCallback((selectedEntry) => {
|
|
111
|
+
const selectOptionAndCleanUp = useCallback((selectedEntry: TOption) => {
|
|
81
112
|
editor.update(() => {
|
|
82
113
|
const textNodeContainingQuery =
|
|
83
114
|
resolution.match != null && shouldSplitNodeWithQuery
|
|
@@ -92,7 +123,7 @@ export function LexicalMenu(props) {
|
|
|
92
123
|
});
|
|
93
124
|
}, [editor, shouldSplitNodeWithQuery, resolution.match, onSelectOption, close]);
|
|
94
125
|
|
|
95
|
-
const updateSelectedIndex = useCallback((index) => {
|
|
126
|
+
const updateSelectedIndex = useCallback((index: number) => {
|
|
96
127
|
const rootElem = editor.getRootElement();
|
|
97
128
|
if (rootElem !== null) {
|
|
98
129
|
rootElem.setAttribute('aria-activedescendant', 'typeahead-item-' + index);
|