@moldable-ai/editor 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +101 -0
  2. package/dist/components/floating-toolbar.d.ts +2 -0
  3. package/dist/components/floating-toolbar.d.ts.map +1 -0
  4. package/dist/components/floating-toolbar.js +109 -0
  5. package/dist/components/markdown-editor.d.ts +15 -0
  6. package/dist/components/markdown-editor.d.ts.map +1 -0
  7. package/dist/components/markdown-editor.js +80 -0
  8. package/dist/index.d.ts +10 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +12 -0
  11. package/dist/lib/lexical/auto-link-config.d.ts +21 -0
  12. package/dist/lib/lexical/auto-link-config.d.ts.map +1 -0
  13. package/dist/lib/lexical/auto-link-config.js +23 -0
  14. package/dist/lib/lexical/clickable-link-plugin.d.ts +11 -0
  15. package/dist/lib/lexical/clickable-link-plugin.d.ts.map +1 -0
  16. package/dist/lib/lexical/clickable-link-plugin.js +97 -0
  17. package/dist/lib/lexical/editor-theme.d.ts +38 -0
  18. package/dist/lib/lexical/editor-theme.d.ts.map +1 -0
  19. package/dist/lib/lexical/editor-theme.js +37 -0
  20. package/dist/lib/lexical/floating-toolbar-plugin.d.ts +30 -0
  21. package/dist/lib/lexical/floating-toolbar-plugin.d.ts.map +1 -0
  22. package/dist/lib/lexical/floating-toolbar-plugin.js +237 -0
  23. package/dist/lib/lexical/headless-editor.d.ts +6 -0
  24. package/dist/lib/lexical/headless-editor.d.ts.map +1 -0
  25. package/dist/lib/lexical/headless-editor.js +30 -0
  26. package/dist/lib/lexical/markdown-transformers.d.ts +40 -0
  27. package/dist/lib/lexical/markdown-transformers.d.ts.map +1 -0
  28. package/dist/lib/lexical/markdown-transformers.js +52 -0
  29. package/dist/lib/lexical/split-node-at-selection.d.ts +9 -0
  30. package/dist/lib/lexical/split-node-at-selection.d.ts.map +1 -0
  31. package/dist/lib/lexical/split-node-at-selection.js +224 -0
  32. package/dist/lib/lexical/sync-plugin.d.ts +11 -0
  33. package/dist/lib/lexical/sync-plugin.d.ts.map +1 -0
  34. package/dist/lib/lexical/sync-plugin.js +45 -0
  35. package/package.json +81 -0
  36. package/src/styles/index.css +71 -0
@@ -0,0 +1,237 @@
1
+ 'use client';
2
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { $splitNodesAtSelectionBoundaries } from './split-node-at-selection';
5
+ import { $createCodeNode } from '@lexical/code';
6
+ import { $isListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode, } from '@lexical/list';
7
+ import { $createHeadingNode, $createQuoteNode, $isHeadingNode, } from '@lexical/rich-text';
8
+ import { $setBlocksType } from '@lexical/selection';
9
+ import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
10
+ import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND, $createParagraphNode as createParagraphNode, } from 'lexical';
11
+ export function useFloatingToolbar() {
12
+ const [editor] = useLexicalComposerContext();
13
+ const [isVisible, setIsVisible] = useState(false);
14
+ const [isBold, setIsBold] = useState(false);
15
+ const [isItalic, setIsItalic] = useState(false);
16
+ const [isUnderline, setIsUnderline] = useState(false);
17
+ const [isStrikethrough, setIsStrikethrough] = useState(false);
18
+ const [isCode, setIsCode] = useState(false);
19
+ const [blockType, setBlockType] = useState('paragraph');
20
+ const [position, setPosition] = useState(null);
21
+ const updateToolbar = useCallback(() => {
22
+ const selection = $getSelection();
23
+ // Hide toolbar if editor is not editable (read-only mode)
24
+ if (!editor.isEditable()) {
25
+ setIsVisible(false);
26
+ setPosition(null);
27
+ return;
28
+ }
29
+ if ($isRangeSelection(selection)) {
30
+ const anchorNode = selection.anchor.getNode();
31
+ const element = anchorNode.getKey() === 'root'
32
+ ? anchorNode
33
+ : anchorNode.getTopLevelElementOrThrow();
34
+ const elementKey = element.getKey();
35
+ const elementDOM = editor.getElementByKey(elementKey);
36
+ // Check if selection is empty (no text selected)
37
+ const isCollapsed = selection.isCollapsed();
38
+ if (elementDOM !== null && !isCollapsed) {
39
+ setIsVisible(true);
40
+ // Update text format states
41
+ setIsBold(selection.hasFormat('bold'));
42
+ setIsItalic(selection.hasFormat('italic'));
43
+ setIsUnderline(selection.hasFormat('underline'));
44
+ setIsStrikethrough(selection.hasFormat('strikethrough'));
45
+ setIsCode(selection.hasFormat('code'));
46
+ // Update block type
47
+ if ($isHeadingNode(element)) {
48
+ const tag = element.getTag();
49
+ setBlockType(tag);
50
+ }
51
+ else if ($isListNode(element)) {
52
+ const parentList = $getNearestNodeOfType(anchorNode, ListNode);
53
+ const type = parentList
54
+ ? parentList.getListType()
55
+ : element.getListType();
56
+ if (type === 'number') {
57
+ setBlockType('number');
58
+ }
59
+ else if (type === 'check') {
60
+ setBlockType('check');
61
+ }
62
+ else {
63
+ setBlockType('bullet');
64
+ }
65
+ }
66
+ else {
67
+ const type = element.getType();
68
+ if (type === 'quote') {
69
+ setBlockType('quote');
70
+ }
71
+ else if (type === 'code') {
72
+ setBlockType('code');
73
+ }
74
+ else {
75
+ setBlockType('paragraph');
76
+ }
77
+ }
78
+ // Calculate position from native selection
79
+ const nativeSelection = window.getSelection();
80
+ const rootElement = editor.getRootElement();
81
+ if (nativeSelection !== null &&
82
+ nativeSelection.rangeCount > 0 &&
83
+ rootElement !== null) {
84
+ const range = nativeSelection.getRangeAt(0);
85
+ const rect = range.getBoundingClientRect();
86
+ if (rect.width > 0 || rect.height > 0) {
87
+ // Use fixed positioning relative to viewport
88
+ // Position at start (left) and bottom of selection
89
+ setPosition({
90
+ top: rect.bottom,
91
+ left: rect.left,
92
+ });
93
+ }
94
+ }
95
+ }
96
+ else {
97
+ setIsVisible(false);
98
+ setPosition(null);
99
+ }
100
+ }
101
+ else {
102
+ setIsVisible(false);
103
+ setPosition(null);
104
+ }
105
+ }, [editor]);
106
+ useEffect(() => {
107
+ return mergeRegister(editor.registerUpdateListener(({ editorState }) => {
108
+ editorState.read(() => {
109
+ updateToolbar();
110
+ });
111
+ }), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
112
+ updateToolbar();
113
+ return false;
114
+ }, COMMAND_PRIORITY_LOW));
115
+ }, [editor, updateToolbar]);
116
+ const formatBold = () => {
117
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
118
+ };
119
+ const formatItalic = () => {
120
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
121
+ };
122
+ const formatUnderline = () => {
123
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
124
+ };
125
+ const formatStrikethrough = () => {
126
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
127
+ };
128
+ const formatCode = () => {
129
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
130
+ };
131
+ const formatParagraph = () => {
132
+ editor.update(() => {
133
+ const selection = $getSelection();
134
+ if ($isRangeSelection(selection)) {
135
+ $setBlocksType(selection, () => createParagraphNode());
136
+ }
137
+ });
138
+ };
139
+ const formatHeading = (headingSize) => {
140
+ if (blockType !== headingSize) {
141
+ editor.update(() => {
142
+ const selection = $getSelection();
143
+ if ($isRangeSelection(selection)) {
144
+ // Split nodes at selection boundaries to ensure we only convert
145
+ // the selected portion, not entire blocks
146
+ $splitNodesAtSelectionBoundaries(selection);
147
+ // Get the updated selection after splitting
148
+ const updatedSelection = $getSelection();
149
+ if ($isRangeSelection(updatedSelection)) {
150
+ // Now apply the heading style - this will only affect blocks
151
+ // that are within or intersect with the selection
152
+ $setBlocksType(updatedSelection, () => $createHeadingNode(headingSize));
153
+ }
154
+ }
155
+ });
156
+ }
157
+ };
158
+ const formatBulletList = () => {
159
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
160
+ };
161
+ const formatNumberedList = () => {
162
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
163
+ };
164
+ const formatCheckList = () => {
165
+ editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined);
166
+ };
167
+ const formatQuote = () => {
168
+ if (blockType !== 'quote') {
169
+ editor.update(() => {
170
+ const selection = $getSelection();
171
+ if ($isRangeSelection(selection)) {
172
+ $setBlocksType(selection, () => $createQuoteNode());
173
+ }
174
+ });
175
+ }
176
+ };
177
+ const formatCodeBlock = () => {
178
+ if (blockType !== 'code') {
179
+ editor.update(() => {
180
+ const selection = $getSelection();
181
+ if ($isRangeSelection(selection)) {
182
+ $setBlocksType(selection, () => $createCodeNode());
183
+ }
184
+ });
185
+ }
186
+ };
187
+ const clearFormatting = () => {
188
+ editor.update(() => {
189
+ const selection = $getSelection();
190
+ if ($isRangeSelection(selection)) {
191
+ // Convert to paragraph if it's a heading, quote, or code block
192
+ const shouldConvertToParagraph = blockType !== 'paragraph' &&
193
+ blockType !== 'bullet' &&
194
+ blockType !== 'number';
195
+ if (shouldConvertToParagraph) {
196
+ $setBlocksType(selection, () => createParagraphNode());
197
+ }
198
+ }
199
+ });
200
+ // Dispatch format commands outside the update block
201
+ // Toggle off any active formats
202
+ if (isBold)
203
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
204
+ if (isItalic)
205
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
206
+ if (isUnderline)
207
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
208
+ if (isStrikethrough)
209
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
210
+ if (isCode)
211
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
212
+ };
213
+ return {
214
+ isVisible,
215
+ isBold,
216
+ isItalic,
217
+ isUnderline,
218
+ isStrikethrough,
219
+ isCode,
220
+ blockType,
221
+ editor,
222
+ position,
223
+ onBold: formatBold,
224
+ onItalic: formatItalic,
225
+ onUnderline: formatUnderline,
226
+ onStrikethrough: formatStrikethrough,
227
+ onCode: formatCode,
228
+ onParagraph: formatParagraph,
229
+ onHeading: formatHeading,
230
+ onBulletList: formatBulletList,
231
+ onNumberedList: formatNumberedList,
232
+ onCheckList: formatCheckList,
233
+ onQuote: formatQuote,
234
+ onCodeBlock: formatCodeBlock,
235
+ onClearFormatting: clearFormatting,
236
+ };
237
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Creates a headless Lexical editor for server-side operations.
3
+ * Use this for translation, content extraction, etc.
4
+ */
5
+ export declare function createMoldableHeadlessEditor(): import("lexical").LexicalEditor;
6
+ //# sourceMappingURL=headless-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-editor.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/headless-editor.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,wBAAgB,4BAA4B,oCAkB3C"}
@@ -0,0 +1,30 @@
1
+ import { editorTheme } from './editor-theme';
2
+ import { CodeHighlightNode, CodeNode } from '@lexical/code';
3
+ import { HorizontalRuleNode } from '@lexical/extension';
4
+ import { createHeadlessEditor } from '@lexical/headless';
5
+ import { AutoLinkNode, LinkNode } from '@lexical/link';
6
+ import { ListItemNode, ListNode } from '@lexical/list';
7
+ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
8
+ /**
9
+ * Creates a headless Lexical editor for server-side operations.
10
+ * Use this for translation, content extraction, etc.
11
+ */
12
+ export function createMoldableHeadlessEditor() {
13
+ return createHeadlessEditor({
14
+ nodes: [
15
+ HeadingNode,
16
+ QuoteNode,
17
+ ListNode,
18
+ ListItemNode,
19
+ CodeNode,
20
+ CodeHighlightNode,
21
+ AutoLinkNode,
22
+ LinkNode,
23
+ HorizontalRuleNode,
24
+ ],
25
+ theme: editorTheme,
26
+ onError: (error) => {
27
+ throw error;
28
+ },
29
+ });
30
+ }
@@ -0,0 +1,40 @@
1
+ import type { ElementTransformer, Transformer } from '@lexical/markdown';
2
+ import type { ElementNode } from 'lexical';
3
+ /**
4
+ * Horizontal rule transformer - supports ---, ***, and ___
5
+ */
6
+ export declare const HR: ElementTransformer;
7
+ /**
8
+ * All markdown transformers
9
+ */
10
+ export declare const markdownTransformers: Array<Transformer>;
11
+ /**
12
+ * Options for converting from markdown string
13
+ */
14
+ interface ConvertFromMarkdownOptions {
15
+ markdown: string;
16
+ transformers?: Array<Transformer>;
17
+ node?: ElementNode;
18
+ shouldPreserveNewLines?: boolean;
19
+ shouldMergeAdjacentLines?: boolean;
20
+ }
21
+ /**
22
+ * Options for converting to markdown string
23
+ */
24
+ interface ConvertToMarkdownOptions {
25
+ transformers?: Array<Transformer>;
26
+ node?: ElementNode;
27
+ shouldPreserveNewLines?: boolean;
28
+ }
29
+ /**
30
+ * Renders markdown from a string. The selection is moved to the start after the operation.
31
+ * Uses named arguments for better readability and maintainability.
32
+ */
33
+ export declare function $convertFromMarkdownString({ markdown, transformers, node, shouldPreserveNewLines, shouldMergeAdjacentLines, }: ConvertFromMarkdownOptions): void;
34
+ /**
35
+ * Renders string from markdown. The selection is moved to the start after the operation.
36
+ * Uses named arguments for better readability and maintainability.
37
+ */
38
+ export declare function $convertToMarkdownString({ transformers, node, shouldPreserveNewLines, }?: ConvertToMarkdownOptions): string;
39
+ export {};
40
+ //# sourceMappingURL=markdown-transformers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-transformers.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/markdown-transformers.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACxE,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,SAAS,CAAA;AAEvD;;GAEG;AACH,eAAO,MAAM,EAAE,EAAE,kBAgBhB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,WAAW,CAOnD,CAAA;AAED;;GAEG;AACH,UAAU,0BAA0B;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IACjC,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,wBAAwB,CAAC,EAAE,OAAO,CAAA;CACnC;AAED;;GAEG;AACH,UAAU,wBAAwB;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IACjC,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACjC;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,EACR,YAAmC,EACnC,IAAI,EACJ,sBAA6B,EAC7B,wBAAgC,GACjC,EAAE,0BAA0B,GAAG,IAAI,CAQnC;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,YAAmC,EACnC,IAAI,EACJ,sBAA6B,GAC9B,GAAE,wBAA6B,GAAG,MAAM,CAExC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Markdown Transformers for Lexical Editor
3
+ * Provides import/export functionality with proper newline preservation
4
+ */
5
+ import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode, } from '@lexical/extension';
6
+ import { CHECK_LIST, ELEMENT_TRANSFORMERS, MULTILINE_ELEMENT_TRANSFORMERS, TEXT_FORMAT_TRANSFORMERS, TEXT_MATCH_TRANSFORMERS, $convertFromMarkdownString as lexicalConvertFromMarkdown, $convertToMarkdownString as lexicalConvertToMarkdown, } from '@lexical/markdown';
7
+ /**
8
+ * Horizontal rule transformer - supports ---, ***, and ___
9
+ */
10
+ export const HR = {
11
+ dependencies: [HorizontalRuleNode],
12
+ export: (node) => {
13
+ return $isHorizontalRuleNode(node) ? '***' : null;
14
+ },
15
+ regExp: /^(---|\*\*\*|___)\s?$/,
16
+ replace: (parentNode, _1, _2, isImport) => {
17
+ const line = $createHorizontalRuleNode();
18
+ if (isImport || parentNode.getNextSibling() != null) {
19
+ parentNode.replace(line);
20
+ }
21
+ else {
22
+ parentNode.insertBefore(line);
23
+ }
24
+ line.selectNext();
25
+ },
26
+ type: 'element',
27
+ };
28
+ /**
29
+ * All markdown transformers
30
+ */
31
+ export const markdownTransformers = [
32
+ HR,
33
+ CHECK_LIST,
34
+ ...ELEMENT_TRANSFORMERS,
35
+ ...MULTILINE_ELEMENT_TRANSFORMERS,
36
+ ...TEXT_FORMAT_TRANSFORMERS,
37
+ ...TEXT_MATCH_TRANSFORMERS,
38
+ ];
39
+ /**
40
+ * Renders markdown from a string. The selection is moved to the start after the operation.
41
+ * Uses named arguments for better readability and maintainability.
42
+ */
43
+ export function $convertFromMarkdownString({ markdown, transformers = markdownTransformers, node, shouldPreserveNewLines = true, shouldMergeAdjacentLines = false, }) {
44
+ return lexicalConvertFromMarkdown(markdown, transformers, node, shouldPreserveNewLines, shouldMergeAdjacentLines);
45
+ }
46
+ /**
47
+ * Renders string from markdown. The selection is moved to the start after the operation.
48
+ * Uses named arguments for better readability and maintainability.
49
+ */
50
+ export function $convertToMarkdownString({ transformers = markdownTransformers, node, shouldPreserveNewLines = true, } = {}) {
51
+ return lexicalConvertToMarkdown(transformers, node, shouldPreserveNewLines);
52
+ }
@@ -0,0 +1,9 @@
1
+ import { RangeSelection } from 'lexical';
2
+ /**
3
+ * Splits nodes at the selection boundaries to ensure block-level formatting
4
+ * (like headings) only affects the selected portion, not entire blocks.
5
+ *
6
+ * @param selection The range selection to split nodes for
7
+ */
8
+ export declare function $splitNodesAtSelectionBoundaries(selection: RangeSelection): void;
9
+ //# sourceMappingURL=split-node-at-selection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"split-node-at-selection.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/split-node-at-selection.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,cAAc,EACf,MAAM,SAAS,CAAA;AAEhB;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,SAAS,EAAE,cAAc,GACxB,IAAI,CAmKN"}
@@ -0,0 +1,224 @@
1
+ import { $createParagraphNode, $isParagraphNode, $isTextNode, } from 'lexical';
2
+ /**
3
+ * Splits nodes at the selection boundaries to ensure block-level formatting
4
+ * (like headings) only affects the selected portion, not entire blocks.
5
+ *
6
+ * @param selection The range selection to split nodes for
7
+ */
8
+ export function $splitNodesAtSelectionBoundaries(selection) {
9
+ const anchorNode = selection.anchor.getNode();
10
+ const focusNode = selection.focus.getNode();
11
+ // Get the top-level element nodes
12
+ const anchorElement = anchorNode.getKey() === 'root'
13
+ ? anchorNode
14
+ : anchorNode.getTopLevelElementOrThrow();
15
+ const focusElement = focusNode.getKey() === 'root'
16
+ ? focusNode
17
+ : focusNode.getTopLevelElementOrThrow();
18
+ const anchorOffset = selection.anchor.offset;
19
+ const focusOffset = selection.focus.offset;
20
+ // Split paragraph at anchor (start of selection) if needed
21
+ // We need to split if there's content before the selection start
22
+ if ($isParagraphNode(anchorElement) && $isTextNode(anchorNode)) {
23
+ const shouldSplitAtAnchor = anchorOffset > 0 ||
24
+ (anchorOffset === 0 && anchorElement.getFirstChild() !== anchorNode);
25
+ if (shouldSplitAtAnchor) {
26
+ // If anchor and focus are in the same text node, we need to handle the focus offset
27
+ // after splitting because splitText will modify the original node
28
+ const anchorAndFocusInSameNode = anchorNode === focusNode && $isTextNode(anchorNode);
29
+ const originalFocusOffset = focusOffset;
30
+ const splitNode = $splitParagraphAtTextOffset(anchorElement, anchorNode, anchorOffset);
31
+ // After splitting at anchor, the focus node might have moved to a new paragraph
32
+ // Re-evaluate the focus element and node
33
+ let updatedFocusNode = selection.focus.getNode();
34
+ let updatedFocusOffset = selection.focus.offset;
35
+ // If anchor and focus were in the same node, we need to adjust the focus
36
+ // because splitText creates a new node for text after the split point
37
+ if (anchorAndFocusInSameNode &&
38
+ anchorOffset < originalFocusOffset &&
39
+ splitNode &&
40
+ $isTextNode(splitNode)) {
41
+ // The focus is now in the split node (the new node created by splitText)
42
+ // Calculate the adjusted offset
43
+ const adjustedOffset = originalFocusOffset - anchorOffset;
44
+ const splitNodeText = splitNode.getTextContent();
45
+ if (adjustedOffset >= 0 && adjustedOffset <= splitNodeText.length) {
46
+ updatedFocusNode = splitNode;
47
+ updatedFocusOffset = adjustedOffset;
48
+ // Update the selection to point to the correct node
49
+ selection.focus.set(updatedFocusNode.getKey(), updatedFocusOffset, 'text');
50
+ }
51
+ }
52
+ const updatedFocusElement = updatedFocusNode.getKey() === 'root'
53
+ ? updatedFocusNode
54
+ : updatedFocusNode.getTopLevelElementOrThrow();
55
+ // Now split at focus using the updated references
56
+ if ($isParagraphNode(updatedFocusElement) &&
57
+ $isTextNode(updatedFocusNode)) {
58
+ const focusTextNodeSize = updatedFocusNode.getTextContentSize();
59
+ const paragraphChildren = updatedFocusElement.getChildren();
60
+ const focusNodeIndex = paragraphChildren.indexOf(updatedFocusNode);
61
+ const isFocusAtEndOfTextNode = updatedFocusOffset === focusTextNodeSize;
62
+ const hasNodesAfterFocus = focusNodeIndex < paragraphChildren.length - 1;
63
+ const shouldSplitAtFocus = updatedFocusOffset < focusTextNodeSize ||
64
+ (isFocusAtEndOfTextNode && hasNodesAfterFocus);
65
+ if (shouldSplitAtFocus) {
66
+ // Split at focus - this will move content after focus to a new paragraph
67
+ // The selected text (from 0 to updatedFocusOffset) remains in updatedFocusNode
68
+ $splitParagraphAtTextOffset(updatedFocusElement, updatedFocusNode, updatedFocusOffset);
69
+ // After splitting at focus, updatedFocusNode contains text from 0 to updatedFocusOffset
70
+ // This is the selected text - update the selection to point to it
71
+ if (anchorAndFocusInSameNode && $isTextNode(updatedFocusNode)) {
72
+ selection.anchor.set(updatedFocusNode.getKey(), 0, 'text');
73
+ selection.focus.set(updatedFocusNode.getKey(), updatedFocusOffset, 'text');
74
+ }
75
+ }
76
+ else if (anchorAndFocusInSameNode && $isTextNode(updatedFocusNode)) {
77
+ // No split at focus needed - selection already points to the right node
78
+ // Just ensure anchor is at the start
79
+ selection.anchor.set(updatedFocusNode.getKey(), 0, 'text');
80
+ selection.focus.set(updatedFocusNode.getKey(), updatedFocusOffset, 'text');
81
+ }
82
+ }
83
+ return;
84
+ }
85
+ }
86
+ // If anchor and focus are in different elements, split both
87
+ if (anchorElement !== focusElement) {
88
+ if ($isParagraphNode(focusElement)) {
89
+ // Check if we need to split at focus
90
+ // We need to split if:
91
+ // 1. The focus offset is not at the end of the text node, OR
92
+ // 2. The focus node is not the last child of the paragraph (there's content after it)
93
+ const focusTextNodeSize = $isTextNode(focusNode)
94
+ ? focusNode.getTextContentSize()
95
+ : 0;
96
+ const isFocusAtEndOfTextNode = focusOffset === focusTextNodeSize;
97
+ const isFocusLastChild = focusElement.getLastChild() === focusNode;
98
+ const shouldSplitAtFocus = focusOffset < focusTextNodeSize ||
99
+ (isFocusAtEndOfTextNode && !isFocusLastChild);
100
+ if (shouldSplitAtFocus &&
101
+ focusOffset <= focusElement.getTextContentSize()) {
102
+ $splitParagraphAtTextOffset(focusElement, focusNode, focusOffset);
103
+ }
104
+ }
105
+ }
106
+ else {
107
+ // Same element and we didn't split at anchor - we need to split at focus
108
+ if ($isParagraphNode(focusElement) && $isTextNode(focusNode)) {
109
+ // Check if we need to split at focus
110
+ // We split if there's content after the focus point
111
+ const focusTextNodeSize = focusNode.getTextContentSize();
112
+ const paragraphChildren = focusElement.getChildren();
113
+ const focusNodeIndex = paragraphChildren.indexOf(focusNode);
114
+ const isFocusAtEndOfTextNode = focusOffset === focusTextNodeSize;
115
+ const hasNodesAfterFocus = focusNodeIndex < paragraphChildren.length - 1;
116
+ const shouldSplitAtFocus = focusOffset < focusTextNodeSize ||
117
+ (isFocusAtEndOfTextNode && hasNodesAfterFocus);
118
+ if (shouldSplitAtFocus) {
119
+ $splitParagraphAtTextOffset(focusElement, focusNode, focusOffset);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Splits a paragraph node at a specific text offset.
126
+ * Creates a new paragraph node and moves the content after the offset to it.
127
+ *
128
+ * @param paragraph The paragraph element to split
129
+ * @param textNode The text node containing the offset
130
+ * @param offset The text offset at which to split
131
+ * @returns The first text node in the new paragraph (the split node), or null if no split occurred
132
+ */
133
+ function $splitParagraphAtTextOffset(paragraph, textNode, offset) {
134
+ if (!$isParagraphNode(paragraph)) {
135
+ return null;
136
+ }
137
+ if (!$isTextNode(textNode)) {
138
+ return null;
139
+ }
140
+ const children = paragraph.getChildren();
141
+ const textNodeIndex = children.indexOf(textNode);
142
+ if (textNodeIndex === -1) {
143
+ return null;
144
+ }
145
+ const textNodeSize = textNode.getTextContentSize();
146
+ // If offset is 0 and text node is not the first child, split before the text node
147
+ if (offset === 0 && textNodeIndex > 0) {
148
+ const newParagraph = $createParagraphNode();
149
+ // Move this text node and all subsequent nodes to the new paragraph
150
+ for (let i = textNodeIndex; i < children.length; i++) {
151
+ const node = children[i];
152
+ if (node) {
153
+ node.remove();
154
+ newParagraph.append(node);
155
+ }
156
+ }
157
+ paragraph.insertAfter(newParagraph);
158
+ // Return the first text node in the new paragraph
159
+ const newParagraphChildren = newParagraph.getChildren();
160
+ return newParagraphChildren.find((node) => $isTextNode(node)) || null;
161
+ }
162
+ // If offset is at the end of the text node, we need to split after the text node
163
+ if (offset === textNodeSize) {
164
+ // Check if there are nodes after this text node
165
+ if (textNodeIndex < children.length - 1) {
166
+ const newParagraph = $createParagraphNode();
167
+ // Move all nodes after the text node to the new paragraph
168
+ for (let i = textNodeIndex + 1; i < children.length; i++) {
169
+ const node = children[i];
170
+ if (node) {
171
+ node.remove();
172
+ newParagraph.append(node);
173
+ }
174
+ }
175
+ paragraph.insertAfter(newParagraph);
176
+ // Return the first text node in the new paragraph
177
+ const newParagraphChildren = newParagraph.getChildren();
178
+ return newParagraphChildren.find((node) => $isTextNode(node)) || null;
179
+ }
180
+ return null;
181
+ }
182
+ // Split the text node if offset is within the text node
183
+ if (offset > 0 && offset < textNodeSize) {
184
+ const splitNodes = textNode.splitText(offset);
185
+ if (splitNodes.length === 0) {
186
+ return null;
187
+ }
188
+ // The split node is the first node in splitNodes (the part after the split)
189
+ const splitTextNode = splitNodes[0];
190
+ if (!splitTextNode) {
191
+ return null;
192
+ }
193
+ // Create a new paragraph and move all nodes after the split point to it
194
+ const newParagraph = $createParagraphNode();
195
+ // Get all nodes from the paragraph that should move to the new paragraph
196
+ const nodesToMove = [];
197
+ const updatedChildren = paragraph.getChildren();
198
+ // Find the first node that should be moved (the split text node)
199
+ for (let i = 0; i < updatedChildren.length; i++) {
200
+ const child = updatedChildren[i];
201
+ if (child === splitTextNode) {
202
+ // Found the split point - move this node and all subsequent nodes
203
+ for (let j = i; j < updatedChildren.length; j++) {
204
+ const nodeToMove = updatedChildren[j];
205
+ if (nodeToMove) {
206
+ nodesToMove.push(nodeToMove);
207
+ }
208
+ }
209
+ break;
210
+ }
211
+ }
212
+ // Move nodes to the new paragraph
213
+ if (nodesToMove.length > 0) {
214
+ nodesToMove.forEach((node) => {
215
+ node.remove();
216
+ newParagraph.append(node);
217
+ });
218
+ // Insert the new paragraph after the original paragraph
219
+ paragraph.insertAfter(newParagraph);
220
+ return splitTextNode;
221
+ }
222
+ }
223
+ return null;
224
+ }
@@ -0,0 +1,11 @@
1
+ interface SyncPluginProps {
2
+ value: string;
3
+ initialValueRef: React.MutableRefObject<string>;
4
+ }
5
+ /**
6
+ * Plugin that syncs external value changes with the editor.
7
+ * Handles cases where the value prop changes from outside the editor.
8
+ */
9
+ export declare function SyncPlugin({ value, initialValueRef }: SyncPluginProps): null;
10
+ export {};
11
+ //# sourceMappingURL=sync-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/sync-plugin.tsx"],"names":[],"mappings":"AAWA,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAA;CAChD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,eAAe,GAAG,IAAI,CAsC5E"}