@optilogic/editor 1.0.0-beta.0
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 +87 -0
- package/dist/index.cjs +487 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +95 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +463 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/index.ts +7 -0
- package/src/slate-editor.tsx +659 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { BaseEditor, Descendant } from 'slate';
|
|
3
|
+
import { ReactEditor } from 'slate-react';
|
|
4
|
+
import { HistoryEditor } from 'slate-history';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SlateEditor Component
|
|
8
|
+
*
|
|
9
|
+
* A reusable rich text editor primitive based on Slate.js
|
|
10
|
+
* Provides consistent theming and can be used as an alternative to textarea
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type TagElement = {
|
|
14
|
+
type: "tag";
|
|
15
|
+
tag: string;
|
|
16
|
+
children: [{
|
|
17
|
+
text: "";
|
|
18
|
+
}];
|
|
19
|
+
};
|
|
20
|
+
type ParagraphElement = {
|
|
21
|
+
type: "paragraph";
|
|
22
|
+
children: Descendant[];
|
|
23
|
+
};
|
|
24
|
+
type CustomElement = TagElement | ParagraphElement;
|
|
25
|
+
type CustomText = {
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
declare module "slate" {
|
|
29
|
+
interface CustomTypes {
|
|
30
|
+
Editor: BaseEditor & ReactEditor & HistoryEditor;
|
|
31
|
+
Element: CustomElement;
|
|
32
|
+
Text: CustomText;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
interface SlateEditorRef {
|
|
36
|
+
/** Focus the editor */
|
|
37
|
+
focus: () => void;
|
|
38
|
+
/** Clear the editor content */
|
|
39
|
+
clear: () => void;
|
|
40
|
+
/** Get plain text content */
|
|
41
|
+
getText: () => string;
|
|
42
|
+
/** Insert text at cursor */
|
|
43
|
+
insertText: (text: string) => void;
|
|
44
|
+
/** Insert a tag element */
|
|
45
|
+
insertTag: (tag: string) => void;
|
|
46
|
+
}
|
|
47
|
+
interface SlateEditorProps {
|
|
48
|
+
/** Initial plain text value */
|
|
49
|
+
value?: string;
|
|
50
|
+
/** Callback when content changes (plain text) */
|
|
51
|
+
onChange?: (value: string) => void;
|
|
52
|
+
/** Callback when a tag is created */
|
|
53
|
+
onTagCreate?: (tag: string) => void;
|
|
54
|
+
/** Callback when a tag is deleted */
|
|
55
|
+
onTagDelete?: (tag: string) => void;
|
|
56
|
+
/** Callback when Enter is pressed (without Shift) */
|
|
57
|
+
onSubmit?: (text: string) => void;
|
|
58
|
+
/** Placeholder text */
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
/** Whether the editor is disabled */
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
/** Whether to enable tag detection (# character) */
|
|
63
|
+
enableTags?: boolean;
|
|
64
|
+
/** Minimum number of rows */
|
|
65
|
+
minRows?: number;
|
|
66
|
+
/** Maximum number of rows before scrolling */
|
|
67
|
+
maxRows?: number;
|
|
68
|
+
/** Additional class names */
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Convert Slate content to plain text
|
|
73
|
+
*/
|
|
74
|
+
declare function serializeToText(nodes: Descendant[]): string;
|
|
75
|
+
/**
|
|
76
|
+
* Convert plain text to Slate nodes (preserving existing tags)
|
|
77
|
+
*/
|
|
78
|
+
declare function deserializeFromText(text: string): Descendant[];
|
|
79
|
+
/**
|
|
80
|
+
* SlateEditor component
|
|
81
|
+
*
|
|
82
|
+
* A rich text editor based on Slate.js with support for inline tags.
|
|
83
|
+
* Can be used as a drop-in replacement for textarea with enhanced features.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* <SlateEditor
|
|
87
|
+
* value={content}
|
|
88
|
+
* onChange={setContent}
|
|
89
|
+
* placeholder="Type your message..."
|
|
90
|
+
* onSubmit={(text) => console.log('Submitted:', text)}
|
|
91
|
+
* />
|
|
92
|
+
*/
|
|
93
|
+
declare const SlateEditor: React.ForwardRefExoticComponent<SlateEditorProps & React.RefAttributes<SlateEditorRef>>;
|
|
94
|
+
|
|
95
|
+
export { SlateEditor, type SlateEditorProps, type SlateEditorRef, deserializeFromText, serializeToText };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { BaseEditor, Descendant } from 'slate';
|
|
3
|
+
import { ReactEditor } from 'slate-react';
|
|
4
|
+
import { HistoryEditor } from 'slate-history';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SlateEditor Component
|
|
8
|
+
*
|
|
9
|
+
* A reusable rich text editor primitive based on Slate.js
|
|
10
|
+
* Provides consistent theming and can be used as an alternative to textarea
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type TagElement = {
|
|
14
|
+
type: "tag";
|
|
15
|
+
tag: string;
|
|
16
|
+
children: [{
|
|
17
|
+
text: "";
|
|
18
|
+
}];
|
|
19
|
+
};
|
|
20
|
+
type ParagraphElement = {
|
|
21
|
+
type: "paragraph";
|
|
22
|
+
children: Descendant[];
|
|
23
|
+
};
|
|
24
|
+
type CustomElement = TagElement | ParagraphElement;
|
|
25
|
+
type CustomText = {
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
declare module "slate" {
|
|
29
|
+
interface CustomTypes {
|
|
30
|
+
Editor: BaseEditor & ReactEditor & HistoryEditor;
|
|
31
|
+
Element: CustomElement;
|
|
32
|
+
Text: CustomText;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
interface SlateEditorRef {
|
|
36
|
+
/** Focus the editor */
|
|
37
|
+
focus: () => void;
|
|
38
|
+
/** Clear the editor content */
|
|
39
|
+
clear: () => void;
|
|
40
|
+
/** Get plain text content */
|
|
41
|
+
getText: () => string;
|
|
42
|
+
/** Insert text at cursor */
|
|
43
|
+
insertText: (text: string) => void;
|
|
44
|
+
/** Insert a tag element */
|
|
45
|
+
insertTag: (tag: string) => void;
|
|
46
|
+
}
|
|
47
|
+
interface SlateEditorProps {
|
|
48
|
+
/** Initial plain text value */
|
|
49
|
+
value?: string;
|
|
50
|
+
/** Callback when content changes (plain text) */
|
|
51
|
+
onChange?: (value: string) => void;
|
|
52
|
+
/** Callback when a tag is created */
|
|
53
|
+
onTagCreate?: (tag: string) => void;
|
|
54
|
+
/** Callback when a tag is deleted */
|
|
55
|
+
onTagDelete?: (tag: string) => void;
|
|
56
|
+
/** Callback when Enter is pressed (without Shift) */
|
|
57
|
+
onSubmit?: (text: string) => void;
|
|
58
|
+
/** Placeholder text */
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
/** Whether the editor is disabled */
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
/** Whether to enable tag detection (# character) */
|
|
63
|
+
enableTags?: boolean;
|
|
64
|
+
/** Minimum number of rows */
|
|
65
|
+
minRows?: number;
|
|
66
|
+
/** Maximum number of rows before scrolling */
|
|
67
|
+
maxRows?: number;
|
|
68
|
+
/** Additional class names */
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Convert Slate content to plain text
|
|
73
|
+
*/
|
|
74
|
+
declare function serializeToText(nodes: Descendant[]): string;
|
|
75
|
+
/**
|
|
76
|
+
* Convert plain text to Slate nodes (preserving existing tags)
|
|
77
|
+
*/
|
|
78
|
+
declare function deserializeFromText(text: string): Descendant[];
|
|
79
|
+
/**
|
|
80
|
+
* SlateEditor component
|
|
81
|
+
*
|
|
82
|
+
* A rich text editor based on Slate.js with support for inline tags.
|
|
83
|
+
* Can be used as a drop-in replacement for textarea with enhanced features.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* <SlateEditor
|
|
87
|
+
* value={content}
|
|
88
|
+
* onChange={setContent}
|
|
89
|
+
* placeholder="Type your message..."
|
|
90
|
+
* onSubmit={(text) => console.log('Submitted:', text)}
|
|
91
|
+
* />
|
|
92
|
+
*/
|
|
93
|
+
declare const SlateEditor: React.ForwardRefExoticComponent<SlateEditorProps & React.RefAttributes<SlateEditorRef>>;
|
|
94
|
+
|
|
95
|
+
export { SlateEditor, type SlateEditorProps, type SlateEditorRef, deserializeFromText, serializeToText };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createEditor, Transforms, Editor, Range, Path, Text, Node, Element } from 'slate';
|
|
3
|
+
import { withReact, ReactEditor, Slate, Editable } from 'slate-react';
|
|
4
|
+
import { withHistory } from 'slate-history';
|
|
5
|
+
import { cn } from '@optilogic/core';
|
|
6
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
// src/slate-editor.tsx
|
|
9
|
+
var withTags = (editor) => {
|
|
10
|
+
const { isInline, isVoid, deleteBackward, deleteForward } = editor;
|
|
11
|
+
editor.isInline = (element) => {
|
|
12
|
+
return element.type === "tag" ? true : isInline(element);
|
|
13
|
+
};
|
|
14
|
+
editor.isVoid = (element) => {
|
|
15
|
+
return element.type === "tag" ? true : isVoid(element);
|
|
16
|
+
};
|
|
17
|
+
editor.deleteBackward = (unit) => {
|
|
18
|
+
const { selection } = editor;
|
|
19
|
+
if (selection && Range.isCollapsed(selection)) {
|
|
20
|
+
const [tagEntry] = Editor.nodes(editor, {
|
|
21
|
+
match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "tag"
|
|
22
|
+
});
|
|
23
|
+
if (tagEntry) {
|
|
24
|
+
const [, path] = tagEntry;
|
|
25
|
+
const after = Editor.after(editor, path);
|
|
26
|
+
if (after && Path.equals(selection.anchor.path, after.path)) {
|
|
27
|
+
Transforms.removeNodes(editor, { at: path });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const before = Editor.before(editor, selection);
|
|
32
|
+
if (before) {
|
|
33
|
+
const [beforeTagEntry] = Editor.nodes(editor, {
|
|
34
|
+
at: before,
|
|
35
|
+
match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "tag"
|
|
36
|
+
});
|
|
37
|
+
if (beforeTagEntry) {
|
|
38
|
+
const [, path] = beforeTagEntry;
|
|
39
|
+
Transforms.removeNodes(editor, { at: path });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
deleteBackward(unit);
|
|
45
|
+
};
|
|
46
|
+
editor.deleteForward = (unit) => {
|
|
47
|
+
const { selection } = editor;
|
|
48
|
+
if (selection && Range.isCollapsed(selection)) {
|
|
49
|
+
const after = Editor.after(editor, selection);
|
|
50
|
+
if (after) {
|
|
51
|
+
const [afterTagEntry] = Editor.nodes(editor, {
|
|
52
|
+
at: after,
|
|
53
|
+
match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "tag"
|
|
54
|
+
});
|
|
55
|
+
if (afterTagEntry) {
|
|
56
|
+
const [, path] = afterTagEntry;
|
|
57
|
+
Transforms.removeNodes(editor, { at: path });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
deleteForward(unit);
|
|
63
|
+
};
|
|
64
|
+
return editor;
|
|
65
|
+
};
|
|
66
|
+
function serializeToText(nodes) {
|
|
67
|
+
return nodes.map((n) => {
|
|
68
|
+
if (Text.isText(n)) {
|
|
69
|
+
return n.text;
|
|
70
|
+
}
|
|
71
|
+
if (Element.isElement(n)) {
|
|
72
|
+
if (n.type === "tag") {
|
|
73
|
+
return `#${n.tag}`;
|
|
74
|
+
}
|
|
75
|
+
return serializeToText(n.children);
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
}).join("");
|
|
79
|
+
}
|
|
80
|
+
function deserializeFromText(text) {
|
|
81
|
+
if (!text) {
|
|
82
|
+
return [{ type: "paragraph", children: [{ text: "" }] }];
|
|
83
|
+
}
|
|
84
|
+
const children = [];
|
|
85
|
+
const tagRegex = /#([a-zA-Z0-9@#$_-]+(?:--[a-zA-Z0-9_-]+)?)/g;
|
|
86
|
+
let lastIndex = 0;
|
|
87
|
+
let match;
|
|
88
|
+
while ((match = tagRegex.exec(text)) !== null) {
|
|
89
|
+
if (match.index > lastIndex) {
|
|
90
|
+
children.push({ text: text.substring(lastIndex, match.index) });
|
|
91
|
+
}
|
|
92
|
+
children.push({
|
|
93
|
+
type: "tag",
|
|
94
|
+
tag: match[1],
|
|
95
|
+
children: [{ text: "" }]
|
|
96
|
+
});
|
|
97
|
+
lastIndex = match.index + match[0].length;
|
|
98
|
+
}
|
|
99
|
+
if (lastIndex < text.length) {
|
|
100
|
+
children.push({ text: text.substring(lastIndex) });
|
|
101
|
+
}
|
|
102
|
+
if (children.length === 0) {
|
|
103
|
+
children.push({ text: "" });
|
|
104
|
+
}
|
|
105
|
+
return [{ type: "paragraph", children }];
|
|
106
|
+
}
|
|
107
|
+
var TagElementRenderer = ({
|
|
108
|
+
attributes,
|
|
109
|
+
children,
|
|
110
|
+
element
|
|
111
|
+
}) => {
|
|
112
|
+
return /* @__PURE__ */ jsxs(
|
|
113
|
+
"span",
|
|
114
|
+
{
|
|
115
|
+
...attributes,
|
|
116
|
+
contentEditable: false,
|
|
117
|
+
className: cn(
|
|
118
|
+
"inline-flex items-center",
|
|
119
|
+
"px-1.5 py-0.5 mx-0.5",
|
|
120
|
+
"rounded-md border",
|
|
121
|
+
"bg-accent/20 text-accent border-accent/40",
|
|
122
|
+
"text-sm font-medium",
|
|
123
|
+
"select-none cursor-default"
|
|
124
|
+
),
|
|
125
|
+
children: [
|
|
126
|
+
/* @__PURE__ */ jsx("span", { className: "font-bold", children: "#" }),
|
|
127
|
+
element.tag,
|
|
128
|
+
children
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
var DefaultElement = ({ attributes, children }) => {
|
|
134
|
+
return /* @__PURE__ */ jsx("p", { ...attributes, children });
|
|
135
|
+
};
|
|
136
|
+
var Leaf = ({ attributes, children }) => {
|
|
137
|
+
return /* @__PURE__ */ jsx("span", { ...attributes, children });
|
|
138
|
+
};
|
|
139
|
+
var SlateEditor = React.forwardRef(
|
|
140
|
+
({
|
|
141
|
+
value = "",
|
|
142
|
+
onChange,
|
|
143
|
+
onTagCreate,
|
|
144
|
+
onTagDelete,
|
|
145
|
+
onSubmit,
|
|
146
|
+
placeholder = "Type something...",
|
|
147
|
+
disabled = false,
|
|
148
|
+
enableTags = true,
|
|
149
|
+
minRows = 3,
|
|
150
|
+
maxRows = 8,
|
|
151
|
+
className
|
|
152
|
+
}, ref) => {
|
|
153
|
+
const editor = React.useMemo(
|
|
154
|
+
() => withTags(withHistory(withReact(createEditor()))),
|
|
155
|
+
[]
|
|
156
|
+
);
|
|
157
|
+
const [isCreatingTag, setIsCreatingTag] = React.useState(false);
|
|
158
|
+
const [tagStartPoint, setTagStartPoint] = React.useState(null);
|
|
159
|
+
const [currentTagText, setCurrentTagText] = React.useState("");
|
|
160
|
+
const [editorValue, setEditorValue] = React.useState(
|
|
161
|
+
() => deserializeFromText(value)
|
|
162
|
+
);
|
|
163
|
+
React.useEffect(() => {
|
|
164
|
+
const newValue = deserializeFromText(value);
|
|
165
|
+
const currentText = serializeToText(editorValue);
|
|
166
|
+
if (currentText !== value) {
|
|
167
|
+
setEditorValue(newValue);
|
|
168
|
+
Transforms.removeNodes(editor, { at: [] });
|
|
169
|
+
Transforms.insertNodes(editor, newValue, { at: [0] });
|
|
170
|
+
Transforms.select(editor, Editor.start(editor, []));
|
|
171
|
+
}
|
|
172
|
+
}, [value, editor]);
|
|
173
|
+
React.useImperativeHandle(
|
|
174
|
+
ref,
|
|
175
|
+
() => ({
|
|
176
|
+
focus: () => {
|
|
177
|
+
ReactEditor.focus(editor);
|
|
178
|
+
},
|
|
179
|
+
clear: () => {
|
|
180
|
+
const newValue = [
|
|
181
|
+
{ type: "paragraph", children: [{ text: "" }] }
|
|
182
|
+
];
|
|
183
|
+
setEditorValue(newValue);
|
|
184
|
+
Transforms.select(editor, Editor.start(editor, []));
|
|
185
|
+
},
|
|
186
|
+
getText: () => serializeToText(editorValue),
|
|
187
|
+
insertText: (text) => {
|
|
188
|
+
Transforms.insertText(editor, text);
|
|
189
|
+
},
|
|
190
|
+
insertTag: (tag) => {
|
|
191
|
+
insertTag(editor, tag);
|
|
192
|
+
onTagCreate?.(tag);
|
|
193
|
+
}
|
|
194
|
+
}),
|
|
195
|
+
[editor, editorValue, onTagCreate]
|
|
196
|
+
);
|
|
197
|
+
const handleChange = React.useCallback(
|
|
198
|
+
(newValue) => {
|
|
199
|
+
setEditorValue(newValue);
|
|
200
|
+
if (isCreatingTag && tagStartPoint) {
|
|
201
|
+
const { selection } = editor;
|
|
202
|
+
if (selection && Range.isCollapsed(selection)) {
|
|
203
|
+
const currentPath = selection.anchor.path;
|
|
204
|
+
const currentOffset = selection.anchor.offset;
|
|
205
|
+
if (Path.equals(currentPath, tagStartPoint.path)) {
|
|
206
|
+
try {
|
|
207
|
+
const [textNode] = Editor.node(editor, currentPath);
|
|
208
|
+
if (Text.isText(textNode)) {
|
|
209
|
+
const tagText = textNode.text.substring(
|
|
210
|
+
tagStartPoint.offset + 1,
|
|
211
|
+
currentOffset
|
|
212
|
+
);
|
|
213
|
+
setCurrentTagText(tagText);
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const oldTags = /* @__PURE__ */ new Set();
|
|
221
|
+
const newTags = /* @__PURE__ */ new Set();
|
|
222
|
+
for (const node of Node.descendants(editor, {
|
|
223
|
+
from: [],
|
|
224
|
+
pass: ([n]) => Element.isElement(n) && n.type === "paragraph"
|
|
225
|
+
})) {
|
|
226
|
+
const [n] = node;
|
|
227
|
+
if (Element.isElement(n) && n.type === "tag") {
|
|
228
|
+
oldTags.add(n.tag);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const extractTags = (nodes) => {
|
|
232
|
+
for (const node of nodes) {
|
|
233
|
+
if (Element.isElement(node)) {
|
|
234
|
+
if (node.type === "tag") {
|
|
235
|
+
newTags.add(node.tag);
|
|
236
|
+
}
|
|
237
|
+
if ("children" in node) {
|
|
238
|
+
extractTags(node.children);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
extractTags(newValue);
|
|
244
|
+
for (const tag of oldTags) {
|
|
245
|
+
if (!newTags.has(tag)) {
|
|
246
|
+
onTagDelete?.(tag);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const text = serializeToText(newValue);
|
|
250
|
+
onChange?.(text);
|
|
251
|
+
},
|
|
252
|
+
[editor, onChange, onTagDelete, isCreatingTag, tagStartPoint]
|
|
253
|
+
);
|
|
254
|
+
const insertTag = (editor2, tag) => {
|
|
255
|
+
const tagNode = {
|
|
256
|
+
type: "tag",
|
|
257
|
+
tag,
|
|
258
|
+
children: [{ text: "" }]
|
|
259
|
+
};
|
|
260
|
+
Transforms.insertNodes(editor2, tagNode);
|
|
261
|
+
Transforms.move(editor2);
|
|
262
|
+
Transforms.insertText(editor2, " ");
|
|
263
|
+
};
|
|
264
|
+
const completeTag = React.useCallback(() => {
|
|
265
|
+
if (!isCreatingTag || !tagStartPoint) return;
|
|
266
|
+
const { selection } = editor;
|
|
267
|
+
if (!selection || !Range.isCollapsed(selection)) return;
|
|
268
|
+
const currentPath = selection.anchor.path;
|
|
269
|
+
const currentOffset = selection.anchor.offset;
|
|
270
|
+
if (!Path.equals(currentPath, tagStartPoint.path)) {
|
|
271
|
+
setIsCreatingTag(false);
|
|
272
|
+
setTagStartPoint(null);
|
|
273
|
+
setCurrentTagText("");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const [textNode] = Editor.node(editor, currentPath);
|
|
277
|
+
if (!Text.isText(textNode)) {
|
|
278
|
+
setIsCreatingTag(false);
|
|
279
|
+
setTagStartPoint(null);
|
|
280
|
+
setCurrentTagText("");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const tagText = textNode.text.substring(
|
|
284
|
+
tagStartPoint.offset + 1,
|
|
285
|
+
currentOffset
|
|
286
|
+
);
|
|
287
|
+
if (!tagText.trim()) {
|
|
288
|
+
setIsCreatingTag(false);
|
|
289
|
+
setTagStartPoint(null);
|
|
290
|
+
setCurrentTagText("");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const start = { path: currentPath, offset: tagStartPoint.offset };
|
|
294
|
+
const end = { path: currentPath, offset: currentOffset };
|
|
295
|
+
Transforms.delete(editor, { at: { anchor: start, focus: end } });
|
|
296
|
+
insertTag(editor, tagText.trim());
|
|
297
|
+
onTagCreate?.(tagText.trim());
|
|
298
|
+
setIsCreatingTag(false);
|
|
299
|
+
setTagStartPoint(null);
|
|
300
|
+
setCurrentTagText("");
|
|
301
|
+
}, [editor, isCreatingTag, tagStartPoint, onTagCreate]);
|
|
302
|
+
const cancelTag = React.useCallback(() => {
|
|
303
|
+
setIsCreatingTag(false);
|
|
304
|
+
setTagStartPoint(null);
|
|
305
|
+
setCurrentTagText("");
|
|
306
|
+
}, []);
|
|
307
|
+
const handleKeyDown = React.useCallback(
|
|
308
|
+
(event) => {
|
|
309
|
+
if (isCreatingTag) {
|
|
310
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
311
|
+
event.preventDefault();
|
|
312
|
+
completeTag();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (event.key === "Escape") {
|
|
316
|
+
event.preventDefault();
|
|
317
|
+
cancelTag();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (enableTags && event.key === "#" && !isCreatingTag) {
|
|
322
|
+
const { selection } = editor;
|
|
323
|
+
if (selection && Range.isCollapsed(selection)) {
|
|
324
|
+
setIsCreatingTag(true);
|
|
325
|
+
setCurrentTagText("");
|
|
326
|
+
setTagStartPoint({
|
|
327
|
+
path: selection.anchor.path,
|
|
328
|
+
offset: selection.anchor.offset
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (event.key === "Enter" && !event.shiftKey && !isCreatingTag) {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
const text = serializeToText(editorValue);
|
|
335
|
+
if (text.trim() && onSubmit) {
|
|
336
|
+
onSubmit(text.trim());
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if ((event.key === "Backspace" || event.key === "Delete") && event.ctrlKey) {
|
|
341
|
+
const { selection } = editor;
|
|
342
|
+
if (selection && Range.isCollapsed(selection)) {
|
|
343
|
+
const direction = event.key === "Backspace" ? "backward" : "forward";
|
|
344
|
+
const pointFn = direction === "backward" ? Editor.before : Editor.after;
|
|
345
|
+
const point = pointFn(editor, selection, { unit: "word" });
|
|
346
|
+
if (point) {
|
|
347
|
+
const [tagEntry] = Editor.nodes(editor, {
|
|
348
|
+
at: direction === "backward" ? { anchor: point, focus: selection.anchor } : { anchor: selection.anchor, focus: point },
|
|
349
|
+
match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "tag"
|
|
350
|
+
});
|
|
351
|
+
if (tagEntry) {
|
|
352
|
+
event.preventDefault();
|
|
353
|
+
const [tagNode, tagPath] = tagEntry;
|
|
354
|
+
if (Element.isElement(tagNode) && tagNode.type === "tag") {
|
|
355
|
+
onTagDelete?.(tagNode.tag);
|
|
356
|
+
}
|
|
357
|
+
Transforms.removeNodes(editor, { at: tagPath });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
[
|
|
365
|
+
editor,
|
|
366
|
+
editorValue,
|
|
367
|
+
isCreatingTag,
|
|
368
|
+
enableTags,
|
|
369
|
+
completeTag,
|
|
370
|
+
cancelTag,
|
|
371
|
+
onSubmit,
|
|
372
|
+
onTagDelete
|
|
373
|
+
]
|
|
374
|
+
);
|
|
375
|
+
const renderElement = React.useCallback((props) => {
|
|
376
|
+
switch (props.element.type) {
|
|
377
|
+
case "tag":
|
|
378
|
+
return /* @__PURE__ */ jsx(
|
|
379
|
+
TagElementRenderer,
|
|
380
|
+
{
|
|
381
|
+
...props,
|
|
382
|
+
element: props.element
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
default:
|
|
386
|
+
return /* @__PURE__ */ jsx(DefaultElement, { ...props });
|
|
387
|
+
}
|
|
388
|
+
}, []);
|
|
389
|
+
const renderLeaf = React.useCallback((props) => {
|
|
390
|
+
return /* @__PURE__ */ jsx(Leaf, { ...props });
|
|
391
|
+
}, []);
|
|
392
|
+
const lineHeight = 24;
|
|
393
|
+
const minHeight = lineHeight * minRows;
|
|
394
|
+
const maxHeight = lineHeight * maxRows;
|
|
395
|
+
return /* @__PURE__ */ jsxs(
|
|
396
|
+
"div",
|
|
397
|
+
{
|
|
398
|
+
className: cn(
|
|
399
|
+
"relative w-full rounded-md",
|
|
400
|
+
"bg-transparent",
|
|
401
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
402
|
+
className
|
|
403
|
+
),
|
|
404
|
+
children: [
|
|
405
|
+
/* @__PURE__ */ jsx(
|
|
406
|
+
Slate,
|
|
407
|
+
{
|
|
408
|
+
editor,
|
|
409
|
+
initialValue: editorValue,
|
|
410
|
+
onChange: handleChange,
|
|
411
|
+
children: /* @__PURE__ */ jsx(
|
|
412
|
+
Editable,
|
|
413
|
+
{
|
|
414
|
+
readOnly: disabled,
|
|
415
|
+
placeholder,
|
|
416
|
+
renderElement,
|
|
417
|
+
renderLeaf,
|
|
418
|
+
onKeyDown: handleKeyDown,
|
|
419
|
+
className: cn(
|
|
420
|
+
"w-full outline-none",
|
|
421
|
+
"text-foreground placeholder:text-muted-foreground",
|
|
422
|
+
"leading-6",
|
|
423
|
+
"overflow-y-auto overflow-x-hidden",
|
|
424
|
+
"whitespace-pre-wrap break-words"
|
|
425
|
+
),
|
|
426
|
+
style: {
|
|
427
|
+
minHeight: `${minHeight}px`,
|
|
428
|
+
maxHeight: `${maxHeight}px`,
|
|
429
|
+
wordBreak: "break-word",
|
|
430
|
+
overflowWrap: "break-word"
|
|
431
|
+
},
|
|
432
|
+
spellCheck: true,
|
|
433
|
+
autoCorrect: "off",
|
|
434
|
+
autoCapitalize: "off"
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
),
|
|
439
|
+
isCreatingTag && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-full left-0 mb-2 flex items-center gap-2.5 animate-in fade-in-0 slide-in-from-bottom-1 duration-150", children: [
|
|
440
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center px-2 py-1 rounded-md border shadow-sm bg-accent/20 border-accent/40", children: [
|
|
441
|
+
/* @__PURE__ */ jsx("span", { className: "font-bold text-sm text-accent", children: "#" }),
|
|
442
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium min-w-[2ch] text-accent", children: currentTagText || /* @__PURE__ */ jsx("span", { className: "opacity-50 italic", children: "tag" }) })
|
|
443
|
+
] }),
|
|
444
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/80 text-[11px]", children: [
|
|
445
|
+
/* @__PURE__ */ jsx("kbd", { className: "px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono mr-1", children: "Enter" }),
|
|
446
|
+
"or",
|
|
447
|
+
/* @__PURE__ */ jsx("kbd", { className: "px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono mx-1", children: "Space" }),
|
|
448
|
+
"to add",
|
|
449
|
+
/* @__PURE__ */ jsx("span", { className: "mx-1.5 text-muted-foreground/50", children: "\xB7" }),
|
|
450
|
+
/* @__PURE__ */ jsx("kbd", { className: "px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono mr-1", children: "Esc" }),
|
|
451
|
+
"to cancel"
|
|
452
|
+
] })
|
|
453
|
+
] })
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
);
|
|
459
|
+
SlateEditor.displayName = "SlateEditor";
|
|
460
|
+
|
|
461
|
+
export { SlateEditor, deserializeFromText, serializeToText };
|
|
462
|
+
//# sourceMappingURL=index.js.map
|
|
463
|
+
//# sourceMappingURL=index.js.map
|