@idealyst/markdown 1.2.38 → 1.2.40
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 +74 -5
- package/src/Editor/EditorToolbar.web.tsx +470 -0
- package/src/Editor/MarkdownEditor.native.tsx +277 -0
- package/src/Editor/MarkdownEditor.styles.ts +337 -0
- package/src/Editor/MarkdownEditor.web.tsx +388 -0
- package/src/Editor/examples/MarkdownEditorExamples.tsx +235 -0
- package/src/Editor/examples/index.ts +1 -0
- package/src/Editor/index.native.ts +23 -0
- package/src/Editor/index.ts +43 -0
- package/src/Editor/types.ts +202 -0
- package/src/examples/index.ts +1 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useImperativeHandle,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
useRef,
|
|
7
|
+
useId,
|
|
8
|
+
useMemo,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { useEditor, EditorContent } from '@tiptap/react';
|
|
11
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
12
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
13
|
+
import Link from '@tiptap/extension-link';
|
|
14
|
+
import TaskList from '@tiptap/extension-task-list';
|
|
15
|
+
import TaskItem from '@tiptap/extension-task-item';
|
|
16
|
+
import Underline from '@tiptap/extension-underline';
|
|
17
|
+
import { Markdown as TiptapMarkdown } from 'tiptap-markdown';
|
|
18
|
+
import { useUnistyles } from 'react-native-unistyles';
|
|
19
|
+
import { editorStyles, generateTiptapCSS } from './MarkdownEditor.styles';
|
|
20
|
+
import { EditorToolbar } from './EditorToolbar.web';
|
|
21
|
+
import type { MarkdownEditorProps, MarkdownEditorRef } from './types';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_TOOLBAR_ITEMS = [
|
|
24
|
+
'bold',
|
|
25
|
+
'italic',
|
|
26
|
+
'underline',
|
|
27
|
+
'strikethrough',
|
|
28
|
+
'code',
|
|
29
|
+
'heading',
|
|
30
|
+
'bulletList',
|
|
31
|
+
'orderedList',
|
|
32
|
+
'blockquote',
|
|
33
|
+
'codeBlock',
|
|
34
|
+
'link',
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Markdown editor for web using Tiptap.
|
|
39
|
+
*
|
|
40
|
+
* Provides a rich text editing experience with markdown input/output.
|
|
41
|
+
* Uses Tiptap with the tiptap-markdown extension for seamless markdown handling.
|
|
42
|
+
*/
|
|
43
|
+
const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>(
|
|
44
|
+
(
|
|
45
|
+
{
|
|
46
|
+
initialValue = '',
|
|
47
|
+
value,
|
|
48
|
+
onChange,
|
|
49
|
+
onFocus,
|
|
50
|
+
onBlur,
|
|
51
|
+
editable = true,
|
|
52
|
+
autoFocus = false,
|
|
53
|
+
placeholder,
|
|
54
|
+
toolbar = {},
|
|
55
|
+
size = 'md',
|
|
56
|
+
linkIntent = 'primary',
|
|
57
|
+
minHeight,
|
|
58
|
+
maxHeight,
|
|
59
|
+
style,
|
|
60
|
+
testID,
|
|
61
|
+
id,
|
|
62
|
+
accessibilityLabel,
|
|
63
|
+
},
|
|
64
|
+
ref
|
|
65
|
+
) => {
|
|
66
|
+
const styleId = useId();
|
|
67
|
+
const isControlled = value !== undefined;
|
|
68
|
+
const lastValueRef = useRef<string>(value ?? initialValue);
|
|
69
|
+
const initializedRef = useRef(false);
|
|
70
|
+
|
|
71
|
+
// Get theme for CSS generation
|
|
72
|
+
const { theme } = useUnistyles();
|
|
73
|
+
|
|
74
|
+
// Apply style variants
|
|
75
|
+
editorStyles.useVariants({
|
|
76
|
+
size,
|
|
77
|
+
linkIntent,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Generate and inject CSS for Tiptap
|
|
81
|
+
const tiptapCSS = useMemo(() => generateTiptapCSS(theme), [theme]);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const existingStyle = document.getElementById(`tiptap-styles-${styleId}`);
|
|
85
|
+
if (existingStyle) {
|
|
86
|
+
existingStyle.textContent = tiptapCSS;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const styleElement = document.createElement('style');
|
|
91
|
+
styleElement.id = `tiptap-styles-${styleId}`;
|
|
92
|
+
styleElement.textContent = tiptapCSS;
|
|
93
|
+
document.head.appendChild(styleElement);
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
const el = document.getElementById(`tiptap-styles-${styleId}`);
|
|
97
|
+
if (el) {
|
|
98
|
+
document.head.removeChild(el);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}, [tiptapCSS, styleId]);
|
|
102
|
+
|
|
103
|
+
const editor = useEditor({
|
|
104
|
+
extensions: [
|
|
105
|
+
StarterKit.configure({
|
|
106
|
+
heading: {
|
|
107
|
+
levels: [1, 2, 3, 4, 5, 6],
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
Placeholder.configure({
|
|
111
|
+
placeholder: placeholder ?? 'Start writing...',
|
|
112
|
+
}),
|
|
113
|
+
Link.configure({
|
|
114
|
+
openOnClick: false,
|
|
115
|
+
HTMLAttributes: {
|
|
116
|
+
rel: 'noopener noreferrer',
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
TaskList,
|
|
120
|
+
TaskItem.configure({
|
|
121
|
+
nested: true,
|
|
122
|
+
}),
|
|
123
|
+
Underline,
|
|
124
|
+
TiptapMarkdown.configure({
|
|
125
|
+
html: false,
|
|
126
|
+
transformPastedText: true,
|
|
127
|
+
transformCopiedText: true,
|
|
128
|
+
}),
|
|
129
|
+
],
|
|
130
|
+
content: '', // Start empty, we'll set content after creation
|
|
131
|
+
editable,
|
|
132
|
+
autofocus: autoFocus,
|
|
133
|
+
onUpdate: ({ editor: ed }) => {
|
|
134
|
+
const markdown = ed.storage.markdown.getMarkdown();
|
|
135
|
+
lastValueRef.current = markdown;
|
|
136
|
+
onChange?.(markdown);
|
|
137
|
+
},
|
|
138
|
+
onFocus: () => onFocus?.(),
|
|
139
|
+
onBlur: () => onBlur?.(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Set initial content after editor is created
|
|
143
|
+
// tiptap-markdown's setContent handles markdown parsing
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (editor && !initializedRef.current) {
|
|
146
|
+
const initialMarkdown = value ?? initialValue;
|
|
147
|
+
if (initialMarkdown) {
|
|
148
|
+
// Use setContent which tiptap-markdown patches to handle markdown
|
|
149
|
+
editor.commands.setContent(initialMarkdown);
|
|
150
|
+
lastValueRef.current = initialMarkdown;
|
|
151
|
+
}
|
|
152
|
+
initializedRef.current = true;
|
|
153
|
+
}
|
|
154
|
+
}, [editor, value, initialValue]);
|
|
155
|
+
|
|
156
|
+
// Handle controlled value changes
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (isControlled && editor && initializedRef.current && value !== lastValueRef.current) {
|
|
159
|
+
const { from, to } = editor.state.selection;
|
|
160
|
+
// tiptap-markdown patches setContent to handle markdown
|
|
161
|
+
editor.commands.setContent(value ?? '');
|
|
162
|
+
// Try to restore cursor position
|
|
163
|
+
const newDocLength = editor.state.doc.content.size;
|
|
164
|
+
const safeFrom = Math.min(from, newDocLength);
|
|
165
|
+
const safeTo = Math.min(to, newDocLength);
|
|
166
|
+
editor.commands.setTextSelection({ from: safeFrom, to: safeTo });
|
|
167
|
+
lastValueRef.current = value ?? '';
|
|
168
|
+
}
|
|
169
|
+
}, [value, isControlled, editor]);
|
|
170
|
+
|
|
171
|
+
// Handle editable changes
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (editor) {
|
|
174
|
+
editor.setEditable(editable);
|
|
175
|
+
}
|
|
176
|
+
}, [editable, editor]);
|
|
177
|
+
|
|
178
|
+
// Expose ref methods
|
|
179
|
+
useImperativeHandle(
|
|
180
|
+
ref,
|
|
181
|
+
() => ({
|
|
182
|
+
getMarkdown: async () => {
|
|
183
|
+
if (!editor) return '';
|
|
184
|
+
return editor.storage.markdown.getMarkdown();
|
|
185
|
+
},
|
|
186
|
+
setMarkdown: (markdown: string) => {
|
|
187
|
+
if (editor) {
|
|
188
|
+
editor.commands.setContent(markdown);
|
|
189
|
+
lastValueRef.current = markdown;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
focus: () => editor?.commands.focus(),
|
|
193
|
+
blur: () => editor?.commands.blur(),
|
|
194
|
+
isEmpty: async () => editor?.isEmpty ?? true,
|
|
195
|
+
clear: () => {
|
|
196
|
+
editor?.commands.clearContent();
|
|
197
|
+
lastValueRef.current = '';
|
|
198
|
+
},
|
|
199
|
+
undo: () => editor?.commands.undo(),
|
|
200
|
+
redo: () => editor?.commands.redo(),
|
|
201
|
+
}),
|
|
202
|
+
[editor]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const handleToolbarAction = useCallback(
|
|
206
|
+
(action: string) => {
|
|
207
|
+
if (!editor) return;
|
|
208
|
+
|
|
209
|
+
switch (action) {
|
|
210
|
+
case 'bold':
|
|
211
|
+
editor.chain().focus().toggleBold().run();
|
|
212
|
+
break;
|
|
213
|
+
case 'italic':
|
|
214
|
+
editor.chain().focus().toggleItalic().run();
|
|
215
|
+
break;
|
|
216
|
+
case 'underline':
|
|
217
|
+
editor.chain().focus().toggleUnderline().run();
|
|
218
|
+
break;
|
|
219
|
+
case 'strikethrough':
|
|
220
|
+
editor.chain().focus().toggleStrike().run();
|
|
221
|
+
break;
|
|
222
|
+
case 'code':
|
|
223
|
+
editor.chain().focus().toggleCode().run();
|
|
224
|
+
break;
|
|
225
|
+
case 'heading1':
|
|
226
|
+
editor.chain().focus().toggleHeading({ level: 1 }).run();
|
|
227
|
+
break;
|
|
228
|
+
case 'heading2':
|
|
229
|
+
editor.chain().focus().toggleHeading({ level: 2 }).run();
|
|
230
|
+
break;
|
|
231
|
+
case 'heading3':
|
|
232
|
+
editor.chain().focus().toggleHeading({ level: 3 }).run();
|
|
233
|
+
break;
|
|
234
|
+
case 'heading4':
|
|
235
|
+
editor.chain().focus().toggleHeading({ level: 4 }).run();
|
|
236
|
+
break;
|
|
237
|
+
case 'heading5':
|
|
238
|
+
editor.chain().focus().toggleHeading({ level: 5 }).run();
|
|
239
|
+
break;
|
|
240
|
+
case 'heading6':
|
|
241
|
+
editor.chain().focus().toggleHeading({ level: 6 }).run();
|
|
242
|
+
break;
|
|
243
|
+
case 'bulletList':
|
|
244
|
+
editor.chain().focus().toggleBulletList().run();
|
|
245
|
+
break;
|
|
246
|
+
case 'orderedList':
|
|
247
|
+
editor.chain().focus().toggleOrderedList().run();
|
|
248
|
+
break;
|
|
249
|
+
case 'taskList':
|
|
250
|
+
editor.chain().focus().toggleTaskList().run();
|
|
251
|
+
break;
|
|
252
|
+
case 'blockquote':
|
|
253
|
+
editor.chain().focus().toggleBlockquote().run();
|
|
254
|
+
break;
|
|
255
|
+
case 'codeBlock':
|
|
256
|
+
editor.chain().focus().toggleCodeBlock().run();
|
|
257
|
+
break;
|
|
258
|
+
case 'horizontalRule':
|
|
259
|
+
editor.chain().focus().setHorizontalRule().run();
|
|
260
|
+
break;
|
|
261
|
+
case 'link': {
|
|
262
|
+
const previousUrl = editor.getAttributes('link').href;
|
|
263
|
+
const url = window.prompt('URL', previousUrl);
|
|
264
|
+
if (url === null) return;
|
|
265
|
+
if (url === '') {
|
|
266
|
+
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
267
|
+
} else {
|
|
268
|
+
editor
|
|
269
|
+
.chain()
|
|
270
|
+
.focus()
|
|
271
|
+
.extendMarkRange('link')
|
|
272
|
+
.setLink({ href: url })
|
|
273
|
+
.run();
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case 'undo':
|
|
278
|
+
editor.chain().focus().undo().run();
|
|
279
|
+
break;
|
|
280
|
+
case 'redo':
|
|
281
|
+
editor.chain().focus().redo().run();
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
[editor]
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const isActive = useCallback(
|
|
289
|
+
(action: string): boolean => {
|
|
290
|
+
if (!editor) return false;
|
|
291
|
+
|
|
292
|
+
switch (action) {
|
|
293
|
+
case 'bold':
|
|
294
|
+
return editor.isActive('bold');
|
|
295
|
+
case 'italic':
|
|
296
|
+
return editor.isActive('italic');
|
|
297
|
+
case 'underline':
|
|
298
|
+
return editor.isActive('underline');
|
|
299
|
+
case 'strikethrough':
|
|
300
|
+
return editor.isActive('strike');
|
|
301
|
+
case 'code':
|
|
302
|
+
return editor.isActive('code');
|
|
303
|
+
case 'heading1':
|
|
304
|
+
return editor.isActive('heading', { level: 1 });
|
|
305
|
+
case 'heading2':
|
|
306
|
+
return editor.isActive('heading', { level: 2 });
|
|
307
|
+
case 'heading3':
|
|
308
|
+
return editor.isActive('heading', { level: 3 });
|
|
309
|
+
case 'heading4':
|
|
310
|
+
return editor.isActive('heading', { level: 4 });
|
|
311
|
+
case 'heading5':
|
|
312
|
+
return editor.isActive('heading', { level: 5 });
|
|
313
|
+
case 'heading6':
|
|
314
|
+
return editor.isActive('heading', { level: 6 });
|
|
315
|
+
case 'bulletList':
|
|
316
|
+
return editor.isActive('bulletList');
|
|
317
|
+
case 'orderedList':
|
|
318
|
+
return editor.isActive('orderedList');
|
|
319
|
+
case 'taskList':
|
|
320
|
+
return editor.isActive('taskList');
|
|
321
|
+
case 'blockquote':
|
|
322
|
+
return editor.isActive('blockquote');
|
|
323
|
+
case 'codeBlock':
|
|
324
|
+
return editor.isActive('codeBlock');
|
|
325
|
+
case 'link':
|
|
326
|
+
return editor.isActive('link');
|
|
327
|
+
default:
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
[editor]
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Build inline styles
|
|
335
|
+
const containerStyle = editorStyles.container({ size, linkIntent });
|
|
336
|
+
const editorContentStyle = editorStyles.editorContent({ size, linkIntent });
|
|
337
|
+
|
|
338
|
+
const toolbarItems = toolbar.items ?? DEFAULT_TOOLBAR_ITEMS;
|
|
339
|
+
const toolbarDisabledItems = toolbar.disabledItems ?? [];
|
|
340
|
+
const showToolbar = toolbar.visible !== false && editable;
|
|
341
|
+
const toolbarPosition = toolbar.position ?? 'top';
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div
|
|
345
|
+
id={id}
|
|
346
|
+
data-testid={testID}
|
|
347
|
+
aria-label={accessibilityLabel}
|
|
348
|
+
className="tiptap-editor-wrapper"
|
|
349
|
+
style={{
|
|
350
|
+
...containerStyle,
|
|
351
|
+
...(minHeight !== undefined ? { minHeight } : {}),
|
|
352
|
+
...(maxHeight !== undefined ? { maxHeight, overflow: 'auto' } : {}),
|
|
353
|
+
...(style as React.CSSProperties),
|
|
354
|
+
}}
|
|
355
|
+
>
|
|
356
|
+
{showToolbar && toolbarPosition === 'top' && (
|
|
357
|
+
<EditorToolbar
|
|
358
|
+
editor={editor}
|
|
359
|
+
items={toolbarItems}
|
|
360
|
+
disabledItems={toolbarDisabledItems}
|
|
361
|
+
onAction={handleToolbarAction}
|
|
362
|
+
isActive={isActive}
|
|
363
|
+
size={size}
|
|
364
|
+
linkIntent={linkIntent}
|
|
365
|
+
/>
|
|
366
|
+
)}
|
|
367
|
+
<div style={editorContentStyle as React.CSSProperties}>
|
|
368
|
+
<EditorContent editor={editor} />
|
|
369
|
+
</div>
|
|
370
|
+
{showToolbar && toolbarPosition === 'bottom' && (
|
|
371
|
+
<EditorToolbar
|
|
372
|
+
editor={editor}
|
|
373
|
+
items={toolbarItems}
|
|
374
|
+
disabledItems={toolbarDisabledItems}
|
|
375
|
+
onAction={handleToolbarAction}
|
|
376
|
+
isActive={isActive}
|
|
377
|
+
size={size}
|
|
378
|
+
linkIntent={linkIntent}
|
|
379
|
+
/>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
MarkdownEditor.displayName = 'MarkdownEditor';
|
|
387
|
+
|
|
388
|
+
export default MarkdownEditor;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
|
+
import { View, Text, Card, Button, Screen } from '@idealyst/components';
|
|
3
|
+
import { MarkdownEditor } from '../index';
|
|
4
|
+
import type { MarkdownEditorRef } from '../types';
|
|
5
|
+
|
|
6
|
+
const SAMPLE_MARKDOWN = `# Welcome to the Editor
|
|
7
|
+
|
|
8
|
+
This is a **rich text** editor that works with *markdown*.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Bold, italic, underline formatting
|
|
13
|
+
- Headings (H1-H6)
|
|
14
|
+
- Bullet and numbered lists
|
|
15
|
+
- Task lists
|
|
16
|
+
- Code blocks
|
|
17
|
+
- Blockquotes
|
|
18
|
+
- Links
|
|
19
|
+
|
|
20
|
+
### Code Example
|
|
21
|
+
|
|
22
|
+
\`\`\`typescript
|
|
23
|
+
const greeting = "Hello, World!";
|
|
24
|
+
console.log(greeting);
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
> This is a blockquote with some important information.
|
|
28
|
+
|
|
29
|
+
Try editing this content!
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Basic usage example
|
|
34
|
+
*/
|
|
35
|
+
function BasicEditorExample() {
|
|
36
|
+
const [content, setContent] = useState(SAMPLE_MARKDOWN);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Card>
|
|
40
|
+
<View spacing="md" style={{ padding: 16 }}>
|
|
41
|
+
<Text size="lg" weight="semibold">Basic Editor</Text>
|
|
42
|
+
<Text size="sm" color="secondary">
|
|
43
|
+
A simple controlled editor with markdown input/output
|
|
44
|
+
</Text>
|
|
45
|
+
<MarkdownEditor
|
|
46
|
+
value={content}
|
|
47
|
+
onChange={setContent}
|
|
48
|
+
placeholder="Start writing..."
|
|
49
|
+
minHeight={300}
|
|
50
|
+
/>
|
|
51
|
+
</View>
|
|
52
|
+
</Card>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Controlled editor with ref methods
|
|
58
|
+
*/
|
|
59
|
+
function ControlledEditorExample() {
|
|
60
|
+
const [content, setContent] = useState('');
|
|
61
|
+
const [output, setOutput] = useState('');
|
|
62
|
+
const editorRef = useRef<MarkdownEditorRef>(null);
|
|
63
|
+
|
|
64
|
+
const handleClear = () => {
|
|
65
|
+
editorRef.current?.clear();
|
|
66
|
+
setOutput('');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleGetContent = async () => {
|
|
70
|
+
const markdown = await editorRef.current?.getMarkdown();
|
|
71
|
+
setOutput(markdown ?? '');
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleSetContent = () => {
|
|
75
|
+
editorRef.current?.setMarkdown('# New Content\n\nThis was set programmatically.');
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Card>
|
|
80
|
+
<View spacing="md" style={{ padding: 16 }}>
|
|
81
|
+
<Text size="lg" weight="semibold">Editor with Ref Methods</Text>
|
|
82
|
+
<Text size="sm" color="secondary">
|
|
83
|
+
Use ref methods to programmatically control the editor
|
|
84
|
+
</Text>
|
|
85
|
+
<View direction="row" spacing="sm">
|
|
86
|
+
<Button size="sm" type="outlined" onPress={handleClear}>
|
|
87
|
+
Clear
|
|
88
|
+
</Button>
|
|
89
|
+
<Button size="sm" type="outlined" onPress={handleGetContent}>
|
|
90
|
+
Get Markdown
|
|
91
|
+
</Button>
|
|
92
|
+
<Button size="sm" type="outlined" onPress={handleSetContent}>
|
|
93
|
+
Set Content
|
|
94
|
+
</Button>
|
|
95
|
+
</View>
|
|
96
|
+
<MarkdownEditor
|
|
97
|
+
ref={editorRef}
|
|
98
|
+
value={content}
|
|
99
|
+
onChange={setContent}
|
|
100
|
+
placeholder="Type something..."
|
|
101
|
+
minHeight={200}
|
|
102
|
+
/>
|
|
103
|
+
{output ? (
|
|
104
|
+
<View spacing="sm">
|
|
105
|
+
<Text size="sm" weight="semibold">Output:</Text>
|
|
106
|
+
<View style={{ backgroundColor: '#f5f5f5', padding: 12, borderRadius: 8 }}>
|
|
107
|
+
<Text size="sm" style={{ fontFamily: 'monospace' }}>
|
|
108
|
+
{output}
|
|
109
|
+
</Text>
|
|
110
|
+
</View>
|
|
111
|
+
</View>
|
|
112
|
+
) : null}
|
|
113
|
+
</View>
|
|
114
|
+
</Card>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read-only mode example
|
|
120
|
+
*/
|
|
121
|
+
function ReadOnlyEditorExample() {
|
|
122
|
+
return (
|
|
123
|
+
<Card>
|
|
124
|
+
<View spacing="md" style={{ padding: 16 }}>
|
|
125
|
+
<Text size="lg" weight="semibold">Read-Only Mode</Text>
|
|
126
|
+
<Text size="sm" color="secondary">
|
|
127
|
+
Use editable=false for a rich markdown viewer
|
|
128
|
+
</Text>
|
|
129
|
+
<MarkdownEditor
|
|
130
|
+
initialValue={SAMPLE_MARKDOWN}
|
|
131
|
+
editable={false}
|
|
132
|
+
toolbar={{ visible: false }}
|
|
133
|
+
minHeight={250}
|
|
134
|
+
/>
|
|
135
|
+
</View>
|
|
136
|
+
</Card>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Custom toolbar configuration
|
|
142
|
+
*/
|
|
143
|
+
function CustomToolbarExample() {
|
|
144
|
+
const [content, setContent] = useState('');
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Card>
|
|
148
|
+
<View spacing="md" style={{ padding: 16 }}>
|
|
149
|
+
<Text size="lg" weight="semibold">Custom Toolbar</Text>
|
|
150
|
+
<Text size="sm" color="secondary">
|
|
151
|
+
Configure which toolbar items appear and their position
|
|
152
|
+
</Text>
|
|
153
|
+
<MarkdownEditor
|
|
154
|
+
value={content}
|
|
155
|
+
onChange={setContent}
|
|
156
|
+
placeholder="Minimal formatting options..."
|
|
157
|
+
toolbar={{
|
|
158
|
+
items: ['bold', 'italic', 'link', 'bulletList', 'orderedList'],
|
|
159
|
+
position: 'bottom',
|
|
160
|
+
}}
|
|
161
|
+
minHeight={200}
|
|
162
|
+
/>
|
|
163
|
+
</View>
|
|
164
|
+
</Card>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Disabled toolbar items example
|
|
170
|
+
*/
|
|
171
|
+
function DisabledItemsExample() {
|
|
172
|
+
const [content, setContent] = useState('');
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<Card>
|
|
176
|
+
<View spacing="md" style={{ padding: 16 }}>
|
|
177
|
+
<Text size="lg" weight="semibold">Disabled Toolbar Items</Text>
|
|
178
|
+
<Text size="sm" color="secondary">
|
|
179
|
+
Disable specific toolbar items while keeping them visible (grayed out)
|
|
180
|
+
</Text>
|
|
181
|
+
<MarkdownEditor
|
|
182
|
+
value={content}
|
|
183
|
+
onChange={setContent}
|
|
184
|
+
placeholder="Link and image buttons are disabled..."
|
|
185
|
+
toolbar={{
|
|
186
|
+
items: ['bold', 'italic', 'underline', 'link', 'image', 'bulletList', 'codeBlock'],
|
|
187
|
+
disabledItems: ['link', 'image'],
|
|
188
|
+
}}
|
|
189
|
+
minHeight={200}
|
|
190
|
+
/>
|
|
191
|
+
</View>
|
|
192
|
+
</Card>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Editor with all examples combined
|
|
198
|
+
*/
|
|
199
|
+
export function MarkdownEditorExamples() {
|
|
200
|
+
return (
|
|
201
|
+
<Screen>
|
|
202
|
+
<View spacing="lg" padding={12}>
|
|
203
|
+
<Text size="xl" weight="bold">
|
|
204
|
+
Markdown Editor
|
|
205
|
+
</Text>
|
|
206
|
+
<Text color="secondary">
|
|
207
|
+
A cross-platform WYSIWYG markdown editor. Uses Tiptap on web and 10tap-editor on native.
|
|
208
|
+
</Text>
|
|
209
|
+
|
|
210
|
+
<Card>
|
|
211
|
+
<View spacing="sm" style={{ padding: 16 }}>
|
|
212
|
+
<Text size="lg" weight="semibold">Features</Text>
|
|
213
|
+
<View spacing="xs">
|
|
214
|
+
<Text size="sm">• Markdown input/output - no HTML exposed to consumers</Text>
|
|
215
|
+
<Text size="sm">• Rich text editing with formatting toolbar</Text>
|
|
216
|
+
<Text size="sm">• Controlled and uncontrolled modes</Text>
|
|
217
|
+
<Text size="sm">• Ref methods for programmatic control</Text>
|
|
218
|
+
<Text size="sm">• Configurable toolbar items and position</Text>
|
|
219
|
+
<Text size="sm">• Disable specific toolbar items</Text>
|
|
220
|
+
<Text size="sm">• Theme integration via Unistyles</Text>
|
|
221
|
+
</View>
|
|
222
|
+
</View>
|
|
223
|
+
</Card>
|
|
224
|
+
|
|
225
|
+
<BasicEditorExample />
|
|
226
|
+
<ControlledEditorExample />
|
|
227
|
+
<ReadOnlyEditorExample />
|
|
228
|
+
<CustomToolbarExample />
|
|
229
|
+
<DisabledItemsExample />
|
|
230
|
+
</View>
|
|
231
|
+
</Screen>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default MarkdownEditorExamples;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MarkdownEditorExamples } from './MarkdownEditorExamples';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @idealyst/markdown/editor - Cross-platform markdown editor (React Native)
|
|
3
|
+
*
|
|
4
|
+
* Provides a MarkdownEditor component for editing markdown content
|
|
5
|
+
* with a rich text interface on React Native using 10tap-editor.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Main component
|
|
9
|
+
export { default as MarkdownEditor } from './MarkdownEditor.native';
|
|
10
|
+
|
|
11
|
+
// Types
|
|
12
|
+
export type {
|
|
13
|
+
MarkdownEditorProps,
|
|
14
|
+
MarkdownEditorRef,
|
|
15
|
+
ToolbarItem,
|
|
16
|
+
ToolbarConfig,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
// Style types
|
|
20
|
+
export type {
|
|
21
|
+
EditorDynamicProps,
|
|
22
|
+
EditorVariants,
|
|
23
|
+
} from './MarkdownEditor.styles';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @idealyst/markdown/editor - Cross-platform markdown editor
|
|
3
|
+
*
|
|
4
|
+
* Provides a MarkdownEditor component for editing markdown content
|
|
5
|
+
* with a rich text interface on both web and React Native platforms.
|
|
6
|
+
*
|
|
7
|
+
* Web uses Tiptap with tiptap-markdown extension.
|
|
8
|
+
* Native uses 10tap-editor (Tiptap in WebView).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { MarkdownEditor } from '@idealyst/markdown/editor';
|
|
13
|
+
*
|
|
14
|
+
* function App() {
|
|
15
|
+
* const [content, setContent] = useState('# Hello World');
|
|
16
|
+
*
|
|
17
|
+
* return (
|
|
18
|
+
* <MarkdownEditor
|
|
19
|
+
* value={content}
|
|
20
|
+
* onChange={setContent}
|
|
21
|
+
* placeholder="Start writing..."
|
|
22
|
+
* />
|
|
23
|
+
* );
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Main component
|
|
29
|
+
export { default as MarkdownEditor } from './MarkdownEditor.web';
|
|
30
|
+
|
|
31
|
+
// Types
|
|
32
|
+
export type {
|
|
33
|
+
MarkdownEditorProps,
|
|
34
|
+
MarkdownEditorRef,
|
|
35
|
+
ToolbarItem,
|
|
36
|
+
ToolbarConfig,
|
|
37
|
+
} from './types';
|
|
38
|
+
|
|
39
|
+
// Style types
|
|
40
|
+
export type {
|
|
41
|
+
EditorDynamicProps,
|
|
42
|
+
EditorVariants,
|
|
43
|
+
} from './MarkdownEditor.styles';
|