@octanejs/lexical 0.1.2
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/LICENSE +21 -0
- package/README.md +67 -0
- package/package.json +104 -0
- package/src/LexicalAutoEmbedPlugin.tsrx +144 -0
- package/src/LexicalAutoFocusPlugin.tsrx +25 -0
- package/src/LexicalAutoLinkPlugin.tsrx +32 -0
- package/src/LexicalBlockWithAlignableContents.tsrx +85 -0
- package/src/LexicalCharacterLimitPlugin.tsrx +66 -0
- package/src/LexicalCheckListPlugin.tsrx +16 -0
- package/src/LexicalClearEditorPlugin.tsrx +12 -0
- package/src/LexicalClickableLinkPlugin.tsrx +17 -0
- package/src/LexicalCollaborationContext.ts +67 -0
- package/src/LexicalComposer.tsrx +100 -0
- package/src/LexicalComposerContext.ts +49 -0
- package/src/LexicalContentEditable.tsrx +48 -0
- package/src/LexicalDecoratorBlockNode.ts +78 -0
- package/src/LexicalDraggableBlockPlugin.tsrx +499 -0
- package/src/LexicalEditorRefPlugin.tsrx +19 -0
- package/src/LexicalErrorBoundary.tsrx +23 -0
- package/src/LexicalHashtagPlugin.tsrx +17 -0
- package/src/LexicalHistoryPlugin.tsrx +23 -0
- package/src/LexicalHorizontalRuleNode.tsrx +99 -0
- package/src/LexicalHorizontalRulePlugin.tsrx +34 -0
- package/src/LexicalLinkPlugin.tsrx +27 -0
- package/src/LexicalListPlugin.tsrx +37 -0
- package/src/LexicalMarkdownShortcutPlugin.tsrx +39 -0
- package/src/LexicalNestedComposer.tsrx +124 -0
- package/src/LexicalNodeContextMenuPlugin.tsrx +252 -0
- package/src/LexicalNodeEventPlugin.tsrx +48 -0
- package/src/LexicalNodeMenuPlugin.tsrx +84 -0
- package/src/LexicalOnChangePlugin.tsrx +36 -0
- package/src/LexicalPlainTextPlugin.tsrx +33 -0
- package/src/LexicalRichTextPlugin.tsrx +35 -0
- package/src/LexicalSelectionAlwaysOnDisplay.tsrx +11 -0
- package/src/LexicalTabIndentationPlugin.tsrx +21 -0
- package/src/LexicalTableOfContentsPlugin.tsrx +213 -0
- package/src/LexicalTablePlugin.tsrx +67 -0
- package/src/LexicalTypeaheadMenuPlugin.tsrx +141 -0
- package/src/autoEmbedShared.ts +43 -0
- package/src/index.ts +99 -0
- package/src/shared/LexicalContentEditableElement.tsrx +101 -0
- package/src/shared/LexicalDecorators.tsrx +88 -0
- package/src/shared/LexicalMenu.tsrx +275 -0
- package/src/shared/internal.ts +32 -0
- package/src/shared/menuShared.ts +152 -0
- package/src/shared/point.ts +46 -0
- package/src/shared/rect.ts +136 -0
- package/src/shared/useCanShowPlaceholder.ts +38 -0
- package/src/shared/useCharacterLimit.ts +288 -0
- package/src/shared/useDynamicPositioning.ts +72 -0
- package/src/shared/useMenuAnchorRef.ts +131 -0
- package/src/shared/usePlainTextSetup.ts +15 -0
- package/src/shared/useRichTextSetup.ts +16 -0
- package/src/tsrx-modules.d.ts +4 -0
- package/src/typeaheadShared.ts +132 -0
- package/src/types.ts +29 -0
- package/src/useLexicalEditable.ts +22 -0
- package/src/useLexicalIsTextContentEmpty.ts +35 -0
- package/src/useLexicalNodeSelection.ts +91 -0
- package/src/useLexicalSlotRef.ts +26 -0
- package/src/useLexicalSubscription.ts +55 -0
- package/src/useLexicalTextEntity.ts +24 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Ported from @lexical/react/src/LexicalComposer.tsx. Creates a LexicalEditor from
|
|
2
|
+
// initialConfig once (useMemo), seeds its initial state, provides it via context,
|
|
3
|
+
// and renders children. `useLayoutEffect` applies the editable flag on mount.
|
|
4
|
+
import { createLexicalComposerContext, LexicalComposerContext } from './LexicalComposerContext';
|
|
5
|
+
import {
|
|
6
|
+
$createParagraphNode,
|
|
7
|
+
$getRoot,
|
|
8
|
+
$getSelection,
|
|
9
|
+
CAN_USE_DOM,
|
|
10
|
+
createEditor,
|
|
11
|
+
getActiveElement,
|
|
12
|
+
HISTORY_MERGE_TAG,
|
|
13
|
+
} from 'lexical';
|
|
14
|
+
import { useMemo, useLayoutEffect } from 'octane';
|
|
15
|
+
|
|
16
|
+
const HISTORY_MERGE_OPTIONS = { tag: HISTORY_MERGE_TAG };
|
|
17
|
+
|
|
18
|
+
function initializeEditor(editor, initialEditorState) {
|
|
19
|
+
if (initialEditorState === null) {
|
|
20
|
+
return;
|
|
21
|
+
} else if (initialEditorState === undefined) {
|
|
22
|
+
editor.update(() => {
|
|
23
|
+
const root = $getRoot();
|
|
24
|
+
if (root.isEmpty()) {
|
|
25
|
+
const paragraph = $createParagraphNode();
|
|
26
|
+
root.append(paragraph);
|
|
27
|
+
const rootElement = editor.getRootElement();
|
|
28
|
+
// getActiveElement (not document.activeElement) reports the shadow host
|
|
29
|
+
// when the editor lives in a shadow root.
|
|
30
|
+
const activeElement =
|
|
31
|
+
CAN_USE_DOM && rootElement !== null ? getActiveElement(rootElement) : null;
|
|
32
|
+
if ($getSelection() !== null || activeElement !== null && activeElement === rootElement) {
|
|
33
|
+
paragraph.select();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}, HISTORY_MERGE_OPTIONS);
|
|
37
|
+
} else if (initialEditorState !== null) {
|
|
38
|
+
switch (typeof initialEditorState) {
|
|
39
|
+
case 'string': {
|
|
40
|
+
const parsedEditorState = editor.parseEditorState(initialEditorState);
|
|
41
|
+
editor.setEditorState(parsedEditorState, HISTORY_MERGE_OPTIONS);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'object': {
|
|
45
|
+
editor.setEditorState(initialEditorState, HISTORY_MERGE_OPTIONS);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case 'function': {
|
|
49
|
+
editor.update(() => {
|
|
50
|
+
const root = $getRoot();
|
|
51
|
+
if (root.isEmpty()) {
|
|
52
|
+
initialEditorState(editor);
|
|
53
|
+
}
|
|
54
|
+
}, HISTORY_MERGE_OPTIONS);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function LexicalComposer(props) @{
|
|
62
|
+
const initialConfig = props.initialConfig;
|
|
63
|
+
|
|
64
|
+
const composerContext = useMemo(() => {
|
|
65
|
+
const {
|
|
66
|
+
theme,
|
|
67
|
+
namespace,
|
|
68
|
+
nodes,
|
|
69
|
+
onError,
|
|
70
|
+
onWarn,
|
|
71
|
+
editorState: initialEditorState,
|
|
72
|
+
html,
|
|
73
|
+
} = initialConfig;
|
|
74
|
+
|
|
75
|
+
const context = createLexicalComposerContext(null, theme);
|
|
76
|
+
|
|
77
|
+
const editor = createEditor({
|
|
78
|
+
editable: initialConfig.editable,
|
|
79
|
+
html,
|
|
80
|
+
namespace,
|
|
81
|
+
nodes,
|
|
82
|
+
onError: (error) => onError(error, editor),
|
|
83
|
+
...(onWarn ? { onWarn: (error) => onWarn(error, editor) } : {}),
|
|
84
|
+
theme,
|
|
85
|
+
});
|
|
86
|
+
initializeEditor(editor, initialEditorState);
|
|
87
|
+
|
|
88
|
+
return [editor, context];
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
useLayoutEffect(() => {
|
|
92
|
+
const isEditable = initialConfig.editable;
|
|
93
|
+
const [editor] = composerContext;
|
|
94
|
+
editor.setEditable(isEditable !== undefined ? isEditable : true);
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
<LexicalComposerContext.Provider
|
|
98
|
+
value={composerContext}
|
|
99
|
+
>{props.children}</LexicalComposerContext.Provider>
|
|
100
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EditorThemeClasses, LexicalEditor } from 'lexical';
|
|
2
|
+
import { createContext, useContext } from 'octane';
|
|
3
|
+
|
|
4
|
+
// Ported from @lexical/react/src/LexicalComposerContext.ts — pure context, no JSX,
|
|
5
|
+
// so it's an exact translation with `react` → `octane` (octane's createContext /
|
|
6
|
+
// useContext match React's: useContext is keyed by context identity, no hook slot).
|
|
7
|
+
|
|
8
|
+
export type LexicalComposerContextType = {
|
|
9
|
+
getTheme: () => EditorThemeClasses | null | undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type LexicalComposerContextWithEditor = [LexicalEditor, LexicalComposerContextType];
|
|
13
|
+
|
|
14
|
+
export const LexicalComposerContext = createContext<
|
|
15
|
+
LexicalComposerContextWithEditor | null | undefined
|
|
16
|
+
>(null);
|
|
17
|
+
|
|
18
|
+
export function createLexicalComposerContext(
|
|
19
|
+
parent: LexicalComposerContextWithEditor | null | undefined,
|
|
20
|
+
theme: EditorThemeClasses | null | undefined,
|
|
21
|
+
): LexicalComposerContextType {
|
|
22
|
+
let parentContext: LexicalComposerContextType | null = null;
|
|
23
|
+
|
|
24
|
+
if (parent != null) {
|
|
25
|
+
parentContext = parent[1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getTheme(): EditorThemeClasses | null | undefined {
|
|
29
|
+
if (theme != null) {
|
|
30
|
+
return theme;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return parentContext != null ? parentContext.getTheme() : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { getTheme };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useLexicalComposerContext(): LexicalComposerContextWithEditor {
|
|
40
|
+
const composerContext = useContext(LexicalComposerContext);
|
|
41
|
+
|
|
42
|
+
if (composerContext == null) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'LexicalComposerContext.useLexicalComposerContext: cannot find a LexicalComposerContext',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return composerContext;
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Ported from @lexical/react/src/LexicalContentEditable.tsx. Reads the editor from
|
|
2
|
+
// context and renders the editable element; an optional `placeholder` is shown
|
|
3
|
+
// while the editor can show one. React's `forwardRef` becomes a plain `ref` prop
|
|
4
|
+
// (spread through to the element).
|
|
5
|
+
import { useLexicalComposerContext } from './LexicalComposerContext';
|
|
6
|
+
import { useCanShowPlaceholder } from './shared/useCanShowPlaceholder';
|
|
7
|
+
import { ContentEditableElement } from './shared/LexicalContentEditableElement.tsrx';
|
|
8
|
+
import { useLayoutEffect, useState } from 'octane';
|
|
9
|
+
|
|
10
|
+
// Re-export the lower-level element (matches @lexical/react/LexicalContentEditable).
|
|
11
|
+
export { ContentEditableElement } from './shared/LexicalContentEditableElement.tsrx';
|
|
12
|
+
|
|
13
|
+
function Placeholder(props) {
|
|
14
|
+
const editor = props.editor;
|
|
15
|
+
const showPlaceholder = useCanShowPlaceholder(editor);
|
|
16
|
+
const [isEditable, setEditable] = useState(editor.isEditable());
|
|
17
|
+
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
setEditable(editor.isEditable());
|
|
20
|
+
return editor.registerEditableListener((currentIsEditable) => {
|
|
21
|
+
setEditable(currentIsEditable);
|
|
22
|
+
});
|
|
23
|
+
}, [editor]);
|
|
24
|
+
|
|
25
|
+
if (!showPlaceholder) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = props.content;
|
|
30
|
+
let placeholder = null;
|
|
31
|
+
if (typeof content === 'function') {
|
|
32
|
+
placeholder = content(isEditable);
|
|
33
|
+
} else if (content !== null) {
|
|
34
|
+
placeholder = content;
|
|
35
|
+
}
|
|
36
|
+
if (placeholder === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return <div aria-hidden="true">{placeholder}</div>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ContentEditable(props) @{
|
|
43
|
+
const [editor] = useLexicalComposerContext();
|
|
44
|
+
<>
|
|
45
|
+
<ContentEditableElement editor={editor} {...props} />
|
|
46
|
+
{props.placeholder != null ? <Placeholder editor={editor} content={props.placeholder} /> : null}
|
|
47
|
+
</>
|
|
48
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Ported from @lexical/react/src/LexicalDecoratorBlockNode.ts. Framework-agnostic
|
|
2
|
+
// apart from the decorate() return type — React typed it `JSX.Element`; octane's
|
|
3
|
+
// decorate() returns a renderable, so the type parameter is left open (subclasses
|
|
4
|
+
// pin it). Pair with BlockWithAlignableContents for selection + alignment.
|
|
5
|
+
import type {
|
|
6
|
+
ElementFormatType,
|
|
7
|
+
LexicalNode,
|
|
8
|
+
LexicalUpdateJSON,
|
|
9
|
+
NodeKey,
|
|
10
|
+
SerializedLexicalNode,
|
|
11
|
+
Spread,
|
|
12
|
+
} from 'lexical';
|
|
13
|
+
|
|
14
|
+
import { DecoratorNode } from 'lexical';
|
|
15
|
+
|
|
16
|
+
export type SerializedDecoratorBlockNode = Spread<
|
|
17
|
+
{
|
|
18
|
+
format: ElementFormatType;
|
|
19
|
+
},
|
|
20
|
+
SerializedLexicalNode
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
export class DecoratorBlockNode extends DecoratorNode<unknown> {
|
|
24
|
+
__format: ElementFormatType;
|
|
25
|
+
|
|
26
|
+
constructor(format?: ElementFormatType, key?: NodeKey) {
|
|
27
|
+
super(key);
|
|
28
|
+
this.__format = format || '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterCloneFrom(prevNode: this): void {
|
|
32
|
+
super.afterCloneFrom(prevNode);
|
|
33
|
+
this.__format = prevNode.__format;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
exportJSON(): SerializedDecoratorBlockNode {
|
|
37
|
+
return {
|
|
38
|
+
...super.exportJSON(),
|
|
39
|
+
format: this.__format || '',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedDecoratorBlockNode>): this {
|
|
44
|
+
return super.updateFromJSON(serializedNode).setFormat(serializedNode.format || '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
canIndent(): false {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
createDOM(): HTMLElement {
|
|
52
|
+
return document.createElement('div');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
updateDOM(): false {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setFormat(format: ElementFormatType): this {
|
|
60
|
+
const self = this.getWritable();
|
|
61
|
+
self.__format = format;
|
|
62
|
+
return self;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getFormat(): ElementFormatType {
|
|
66
|
+
return this.getLatest().__format;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isInline(): false {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function $isDecoratorBlockNode(
|
|
75
|
+
node: LexicalNode | null | undefined,
|
|
76
|
+
): node is DecoratorBlockNode {
|
|
77
|
+
return node instanceof DecoratorBlockNode;
|
|
78
|
+
}
|