@apollohg/react-native-prose-editor 0.1.1 → 0.3.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.
Files changed (53) hide show
  1. package/README.md +12 -7
  2. package/android/build.gradle +7 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
  5. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
  9. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
  10. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
  12. package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
  13. package/dist/EditorTheme.js +29 -0
  14. package/dist/EditorToolbar.d.ts +129 -0
  15. package/dist/EditorToolbar.js +394 -0
  16. package/dist/NativeEditorBridge.d.ts +242 -0
  17. package/dist/NativeEditorBridge.js +647 -0
  18. package/dist/NativeRichTextEditor.d.ts +142 -0
  19. package/dist/NativeRichTextEditor.js +649 -0
  20. package/dist/YjsCollaboration.d.ts +83 -0
  21. package/dist/YjsCollaboration.js +585 -0
  22. package/dist/addons.d.ts +70 -0
  23. package/dist/addons.js +77 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +26 -0
  26. package/dist/schemas.d.ts +35 -0
  27. package/{src/schemas.ts → dist/schemas.js} +62 -27
  28. package/dist/useNativeEditor.d.ts +40 -0
  29. package/dist/useNativeEditor.js +117 -0
  30. package/ios/EditorAddons.swift +26 -3
  31. package/ios/EditorCore.xcframework/Info.plist +5 -5
  32. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  33. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  34. package/ios/EditorLayoutManager.swift +236 -0
  35. package/ios/EditorTheme.swift +51 -1
  36. package/ios/Generated_editor_core.swift +270 -2
  37. package/ios/NativeEditorExpoView.swift +612 -45
  38. package/ios/NativeEditorModule.swift +81 -0
  39. package/ios/PositionBridge.swift +22 -0
  40. package/ios/RenderBridge.swift +427 -39
  41. package/ios/RichTextEditorView.swift +1342 -18
  42. package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
  43. package/package.json +80 -64
  44. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  45. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  46. package/rust/android/x86_64/libeditor_core.so +0 -0
  47. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
  48. package/src/EditorToolbar.tsx +0 -620
  49. package/src/NativeEditorBridge.ts +0 -607
  50. package/src/NativeRichTextEditor.tsx +0 -951
  51. package/src/addons.ts +0 -158
  52. package/src/index.ts +0 -63
  53. package/src/useNativeEditor.ts +0 -173
@@ -1,620 +0,0 @@
1
- import { MaterialIcons } from '@expo/vector-icons';
2
- import React, { useCallback } from 'react';
3
- import {
4
- ScrollView,
5
- StyleSheet,
6
- Text,
7
- TouchableOpacity,
8
- View,
9
- } from 'react-native';
10
-
11
- import type { ActiveState, HistoryState } from './NativeEditorBridge';
12
- import type { EditorToolbarTheme } from './EditorTheme';
13
-
14
- interface ToolbarButton {
15
- key: string;
16
- label: string;
17
- icon: EditorToolbarIcon;
18
- action: () => void;
19
- isActive?: boolean;
20
- isDisabled?: boolean;
21
- }
22
-
23
- export type EditorToolbarListType = 'bulletList' | 'orderedList';
24
- export type EditorToolbarCommand = 'indentList' | 'outdentList' | 'undo' | 'redo';
25
-
26
- export type EditorToolbarDefaultIconId =
27
- | 'bold'
28
- | 'italic'
29
- | 'underline'
30
- | 'strike'
31
- | 'bulletList'
32
- | 'orderedList'
33
- | 'indentList'
34
- | 'outdentList'
35
- | 'lineBreak'
36
- | 'horizontalRule'
37
- | 'undo'
38
- | 'redo';
39
-
40
- export interface EditorToolbarSFSymbolIcon {
41
- type: 'sfSymbol';
42
- name: string;
43
- }
44
-
45
- export interface EditorToolbarMaterialIcon {
46
- type: 'material';
47
- name: string;
48
- }
49
-
50
- export type EditorToolbarIcon =
51
- | {
52
- type: 'default';
53
- id: EditorToolbarDefaultIconId;
54
- }
55
- | {
56
- type: 'glyph';
57
- text: string;
58
- }
59
- | {
60
- type: 'platform';
61
- ios?: EditorToolbarSFSymbolIcon;
62
- android?: EditorToolbarMaterialIcon;
63
- fallbackText?: string;
64
- };
65
-
66
- export type EditorToolbarItem =
67
- | {
68
- type: 'mark';
69
- mark: string;
70
- label: string;
71
- icon: EditorToolbarIcon;
72
- key?: string;
73
- }
74
- | {
75
- type: 'list';
76
- listType: EditorToolbarListType;
77
- label: string;
78
- icon: EditorToolbarIcon;
79
- key?: string;
80
- }
81
- | {
82
- type: 'command';
83
- command: EditorToolbarCommand;
84
- label: string;
85
- icon: EditorToolbarIcon;
86
- key?: string;
87
- }
88
- | {
89
- type: 'node';
90
- nodeType: string;
91
- label: string;
92
- icon: EditorToolbarIcon;
93
- key?: string;
94
- }
95
- | {
96
- type: 'separator';
97
- key?: string;
98
- }
99
- | {
100
- type: 'action';
101
- key: string;
102
- label: string;
103
- icon: EditorToolbarIcon;
104
- isActive?: boolean;
105
- isDisabled?: boolean;
106
- };
107
-
108
- function defaultIcon(id: EditorToolbarDefaultIconId): EditorToolbarIcon {
109
- return { type: 'default', id };
110
- }
111
-
112
- export const DEFAULT_EDITOR_TOOLBAR_ITEMS: readonly EditorToolbarItem[] = [
113
- { type: 'mark', mark: 'bold', label: 'Bold', icon: defaultIcon('bold') },
114
- { type: 'mark', mark: 'italic', label: 'Italic', icon: defaultIcon('italic') },
115
- { type: 'mark', mark: 'underline', label: 'Underline', icon: defaultIcon('underline') },
116
- { type: 'mark', mark: 'strike', label: 'Strikethrough', icon: defaultIcon('strike') },
117
- { type: 'separator' },
118
- { type: 'list', listType: 'bulletList', label: 'Bullet List', icon: defaultIcon('bulletList') },
119
- { type: 'list', listType: 'orderedList', label: 'Ordered List', icon: defaultIcon('orderedList') },
120
- { type: 'command', command: 'indentList', label: 'Indent List', icon: defaultIcon('indentList') },
121
- { type: 'command', command: 'outdentList', label: 'Outdent List', icon: defaultIcon('outdentList') },
122
- { type: 'node', nodeType: 'hardBreak', label: 'Line Break', icon: defaultIcon('lineBreak') },
123
- { type: 'node', nodeType: 'horizontalRule', label: 'Horizontal Rule', icon: defaultIcon('horizontalRule') },
124
- { type: 'separator' },
125
- { type: 'command', command: 'undo', label: 'Undo', icon: defaultIcon('undo') },
126
- { type: 'command', command: 'redo', label: 'Redo', icon: defaultIcon('redo') },
127
- ] as const;
128
-
129
- export interface EditorToolbarProps {
130
- /** Currently active marks and nodes from the Rust engine. */
131
- activeState: ActiveState;
132
- /** Current undo/redo availability. */
133
- historyState: HistoryState;
134
- /** Toggle bold mark. */
135
- onToggleBold: () => void;
136
- /** Toggle italic mark. */
137
- onToggleItalic: () => void;
138
- /** Toggle underline mark. */
139
- onToggleUnderline: () => void;
140
- /** Toggle strikethrough mark. */
141
- onToggleStrike: () => void;
142
- /** Toggle bullet list. */
143
- onToggleBulletList?: () => void;
144
- /** Toggle ordered list. */
145
- onToggleOrderedList?: () => void;
146
- /** Indent the current list item. */
147
- onIndentList?: () => void;
148
- /** Outdent the current list item. */
149
- onOutdentList?: () => void;
150
- /** Insert horizontal rule. */
151
- onInsertHorizontalRule?: () => void;
152
- /** Insert inline hard break. */
153
- onInsertLineBreak?: () => void;
154
- /** Undo the last operation. */
155
- onUndo: () => void;
156
- /** Redo the last undone operation. */
157
- onRedo: () => void;
158
- /** Generic mark toggle handler used by configurable mark buttons. */
159
- onToggleMark?: (mark: string) => void;
160
- /** Generic list toggle handler used by configurable list buttons. */
161
- onToggleListType?: (listType: EditorToolbarListType) => void;
162
- /** Generic node insertion handler used by configurable node buttons. */
163
- onInsertNodeType?: (nodeType: string) => void;
164
- /** Generic command handler used by configurable command buttons. */
165
- onRunCommand?: (command: EditorToolbarCommand) => void;
166
- /** Generic action handler for arbitrary JS-defined toolbar buttons. */
167
- onToolbarAction?: (key: string) => void;
168
- /** Displayed toolbar items, in order. Defaults to the built-in toolbar. */
169
- toolbarItems?: readonly EditorToolbarItem[];
170
- /** Optional theme overrides for toolbar chrome and button colors. */
171
- theme?: EditorToolbarTheme;
172
- /** Whether to render the built-in top separator line. */
173
- showTopBorder?: boolean;
174
- }
175
-
176
- const BUTTON_HIT = 44;
177
- const BUTTON_VISIBLE = 32;
178
- const TOOLBAR_PADDING_H = 12;
179
- const TOOLBAR_PADDING_V = 4;
180
-
181
- const ACTIVE_BG = 'rgba(0, 122, 255, 0.12)';
182
- const ACTIVE_COLOR = '#007AFF';
183
- const DEFAULT_COLOR = '#666666';
184
- const DISABLED_COLOR = '#C7C7CC';
185
- const SEPARATOR_COLOR = '#E5E5EA';
186
- const TOOLBAR_BG = '#FFFFFF';
187
- const TOOLBAR_BORDER = '#E5E5EA';
188
- const TOOLBAR_RADIUS = 0;
189
- const BUTTON_RADIUS = 6;
190
-
191
- const DEFAULT_GLYPH_ICONS: Record<EditorToolbarDefaultIconId, string> = {
192
- bold: 'B',
193
- italic: 'I',
194
- underline: 'U',
195
- strike: 'S',
196
- bulletList: '•≡',
197
- orderedList: '1.',
198
- indentList: '→',
199
- outdentList: '←',
200
- lineBreak: '↵',
201
- horizontalRule: '—',
202
- undo: '↩',
203
- redo: '↪',
204
- };
205
-
206
- const DEFAULT_MATERIAL_ICONS: Record<EditorToolbarDefaultIconId, string> = {
207
- bold: 'format-bold',
208
- italic: 'format-italic',
209
- underline: 'format-underlined',
210
- strike: 'strikethrough-s',
211
- bulletList: 'format-list-bulleted',
212
- orderedList: 'format-list-numbered',
213
- indentList: 'format-indent-increase',
214
- outdentList: 'format-indent-decrease',
215
- lineBreak: 'keyboard-return',
216
- horizontalRule: 'horizontal-rule',
217
- undo: 'undo',
218
- redo: 'redo',
219
- };
220
-
221
- export function EditorToolbar({
222
- activeState,
223
- historyState,
224
- onToggleBold,
225
- onToggleItalic,
226
- onToggleUnderline,
227
- onToggleStrike,
228
- onToggleBulletList,
229
- onToggleOrderedList,
230
- onIndentList,
231
- onOutdentList,
232
- onInsertHorizontalRule,
233
- onInsertLineBreak,
234
- onUndo,
235
- onRedo,
236
- onToggleMark,
237
- onToggleListType,
238
- onInsertNodeType,
239
- onRunCommand,
240
- onToolbarAction,
241
- toolbarItems = DEFAULT_EDITOR_TOOLBAR_ITEMS,
242
- theme,
243
- showTopBorder = true,
244
- }: EditorToolbarProps) {
245
- const marks = activeState.marks ?? {};
246
- const nodes = activeState.nodes ?? {};
247
- const commands = activeState.commands ?? {};
248
- const allowedMarks = activeState.allowedMarks ?? [];
249
- const insertableNodes = activeState.insertableNodes ?? [];
250
-
251
- const isMarkActive = useCallback(
252
- (mark: string) => !!marks[mark],
253
- [marks]
254
- );
255
-
256
- const isInList = !!nodes['bulletList'] || !!nodes['orderedList'];
257
- const canIndentList = isInList && !!commands['indentList'];
258
- const canOutdentList = isInList && !!commands['outdentList'];
259
-
260
- const getActionForItem = useCallback(
261
- (item: EditorToolbarItem): (() => void) | null => {
262
- switch (item.type) {
263
- case 'separator':
264
- return null;
265
- case 'mark':
266
- if (onToggleMark) {
267
- return () => onToggleMark(item.mark);
268
- }
269
- switch (item.mark) {
270
- case 'bold':
271
- return onToggleBold;
272
- case 'italic':
273
- return onToggleItalic;
274
- case 'underline':
275
- return onToggleUnderline;
276
- case 'strike':
277
- return onToggleStrike;
278
- default:
279
- return null;
280
- }
281
- case 'list':
282
- if (onToggleListType) {
283
- return () => onToggleListType(item.listType);
284
- }
285
- return item.listType === 'bulletList'
286
- ? onToggleBulletList ?? null
287
- : onToggleOrderedList ?? null;
288
- case 'node':
289
- if (onInsertNodeType) {
290
- return () => onInsertNodeType(item.nodeType);
291
- }
292
- switch (item.nodeType) {
293
- case 'hardBreak':
294
- return onInsertLineBreak ?? null;
295
- case 'horizontalRule':
296
- return onInsertHorizontalRule ?? null;
297
- default:
298
- return null;
299
- }
300
- case 'command':
301
- if (onRunCommand) {
302
- return () => onRunCommand(item.command);
303
- }
304
- switch (item.command) {
305
- case 'indentList':
306
- return onIndentList ?? null;
307
- case 'outdentList':
308
- return onOutdentList ?? null;
309
- case 'undo':
310
- return onUndo;
311
- case 'redo':
312
- return onRedo;
313
- }
314
- case 'action':
315
- return onToolbarAction ? () => onToolbarAction(item.key) : null;
316
- }
317
- },
318
- [
319
- onIndentList,
320
- onInsertHorizontalRule,
321
- onInsertLineBreak,
322
- onInsertNodeType,
323
- onOutdentList,
324
- onRedo,
325
- onRunCommand,
326
- onToolbarAction,
327
- onToggleBold,
328
- onToggleBulletList,
329
- onToggleItalic,
330
- onToggleListType,
331
- onToggleMark,
332
- onToggleOrderedList,
333
- onToggleStrike,
334
- onToggleUnderline,
335
- onUndo,
336
- ]
337
- );
338
-
339
- const makeButtonKey = useCallback(
340
- (
341
- item: Exclude<EditorToolbarItem, { type: 'separator' }>,
342
- index: number
343
- ) =>
344
- item.key ??
345
- (item.type === 'mark'
346
- ? `mark:${item.mark}:${index}`
347
- : item.type === 'list'
348
- ? `list:${item.listType}:${index}`
349
- : item.type === 'command'
350
- ? `command:${item.command}:${index}`
351
- : item.type === 'node'
352
- ? `node:${item.nodeType}:${index}`
353
- : `action:${item.key}:${index}`),
354
- []
355
- );
356
-
357
- const renderedItems: Array<
358
- | { type: 'separator'; key: string }
359
- | { type: 'button'; button: ToolbarButton }
360
- > = [];
361
-
362
- for (let index = 0; index < toolbarItems.length; index += 1) {
363
- const item = toolbarItems[index];
364
- if (item.type === 'separator') {
365
- renderedItems.push({
366
- type: 'separator',
367
- key: item.key ?? `separator:${index}`,
368
- });
369
- continue;
370
- }
371
-
372
- const action = getActionForItem(item);
373
- if (!action) {
374
- continue;
375
- }
376
-
377
- let isActive = false;
378
- let isDisabled = false;
379
- switch (item.type) {
380
- case 'mark':
381
- isActive = isMarkActive(item.mark);
382
- isDisabled = !allowedMarks.includes(item.mark);
383
- break;
384
- case 'list':
385
- isActive = !!nodes[item.listType];
386
- isDisabled = !commands[
387
- item.listType === 'bulletList'
388
- ? 'wrapBulletList'
389
- : 'wrapOrderedList'
390
- ];
391
- break;
392
- case 'command':
393
- switch (item.command) {
394
- case 'indentList':
395
- isDisabled = !canIndentList;
396
- break;
397
- case 'outdentList':
398
- isDisabled = !canOutdentList;
399
- break;
400
- case 'undo':
401
- isDisabled = !historyState.canUndo;
402
- break;
403
- case 'redo':
404
- isDisabled = !historyState.canRedo;
405
- break;
406
- }
407
- break;
408
- case 'action':
409
- isActive = !!item.isActive;
410
- isDisabled = !!item.isDisabled || !onToolbarAction;
411
- break;
412
- case 'node':
413
- isActive = !!nodes[item.nodeType];
414
- isDisabled = !insertableNodes.includes(item.nodeType);
415
- break;
416
- }
417
-
418
- renderedItems.push({
419
- type: 'button',
420
- button: {
421
- key: makeButtonKey(item, index),
422
- label: item.label,
423
- icon: item.icon,
424
- action,
425
- isActive,
426
- isDisabled,
427
- },
428
- });
429
- }
430
-
431
- const compactItems = renderedItems.filter((entry, index, list) => {
432
- if (entry.type !== 'separator') {
433
- return true;
434
- }
435
- const previous = list[index - 1];
436
- const next = list[index + 1];
437
- return previous?.type === 'button' && next?.type === 'button';
438
- });
439
-
440
- const renderButton = ({
441
- key,
442
- label,
443
- icon,
444
- action,
445
- isActive,
446
- isDisabled,
447
- }: ToolbarButton) => {
448
- const activeColor = theme?.buttonActiveColor ?? ACTIVE_COLOR;
449
- const defaultColor = theme?.buttonColor ?? DEFAULT_COLOR;
450
- const disabledColor = theme?.buttonDisabledColor ?? DISABLED_COLOR;
451
- const color = isActive
452
- ? activeColor
453
- : isDisabled
454
- ? disabledColor
455
- : defaultColor;
456
-
457
- return (
458
- <TouchableOpacity
459
- key={key}
460
- onPress={action}
461
- disabled={isDisabled}
462
- style={[
463
- styles.button,
464
- {
465
- borderRadius:
466
- theme?.buttonBorderRadius ?? BUTTON_RADIUS,
467
- },
468
- isActive && {
469
- backgroundColor:
470
- theme?.buttonActiveBackgroundColor ?? ACTIVE_BG,
471
- },
472
- ]}
473
- activeOpacity={0.5}
474
- accessibilityRole="button"
475
- accessibilityLabel={label}
476
- accessibilityState={{ selected: isActive, disabled: isDisabled }}
477
- >
478
- <View>
479
- <ToolbarIcon icon={icon} color={color} />
480
- </View>
481
- </TouchableOpacity>
482
- );
483
- };
484
-
485
- const renderSeparator = (key: string) => (
486
- <View
487
- key={key}
488
- style={[
489
- styles.separator,
490
- theme?.separatorColor != null
491
- ? { backgroundColor: theme.separatorColor }
492
- : null,
493
- ]}
494
- />
495
- );
496
-
497
- return (
498
- <View
499
- style={[
500
- styles.container,
501
- !showTopBorder && styles.containerWithoutTopBorder,
502
- theme?.backgroundColor != null
503
- ? { backgroundColor: theme.backgroundColor }
504
- : null,
505
- theme?.borderColor != null
506
- ? showTopBorder
507
- ? { borderTopColor: theme.borderColor }
508
- : null
509
- : null,
510
- theme?.borderWidth != null
511
- ? showTopBorder
512
- ? { borderTopWidth: theme.borderWidth }
513
- : null
514
- : null,
515
- {
516
- borderRadius: theme?.borderRadius ?? TOOLBAR_RADIUS,
517
- },
518
- ]}
519
- >
520
- <ScrollView
521
- horizontal
522
- showsHorizontalScrollIndicator={false}
523
- contentContainerStyle={styles.scrollContent}
524
- keyboardShouldPersistTaps="always"
525
- >
526
- {compactItems.map((item) =>
527
- item.type === 'separator'
528
- ? renderSeparator(item.key)
529
- : renderButton(item.button)
530
- )}
531
- </ScrollView>
532
- </View>
533
- );
534
- }
535
-
536
- function ToolbarIcon({
537
- icon,
538
- color,
539
- }: {
540
- icon: EditorToolbarIcon;
541
- color: string;
542
- }) {
543
- const materialIconName = resolveMaterialIconName(icon);
544
- if (materialIconName) {
545
- return (
546
- <View style={styles.iconContainer}>
547
- <MaterialIcons name={materialIconName as never} size={20} color={color} />
548
- </View>
549
- );
550
- }
551
-
552
- const glyph = resolveGlyphText(icon) ?? '?';
553
- return (
554
- <View style={styles.iconContainer}>
555
- <Text style={[styles.iconText, { color }]}>{glyph}</Text>
556
- </View>
557
- );
558
- }
559
-
560
- function resolveMaterialIconName(icon: EditorToolbarIcon): string | undefined {
561
- switch (icon.type) {
562
- case 'default':
563
- return DEFAULT_MATERIAL_ICONS[icon.id];
564
- case 'platform':
565
- return icon.android?.type === 'material' ? icon.android.name : undefined;
566
- case 'glyph':
567
- return undefined;
568
- }
569
- }
570
-
571
- function resolveGlyphText(icon: EditorToolbarIcon): string | undefined {
572
- switch (icon.type) {
573
- case 'default':
574
- return DEFAULT_GLYPH_ICONS[icon.id];
575
- case 'glyph':
576
- return icon.text;
577
- case 'platform':
578
- return icon.fallbackText;
579
- }
580
- }
581
-
582
- const styles = StyleSheet.create({
583
- container: {
584
- backgroundColor: TOOLBAR_BG,
585
- borderTopWidth: StyleSheet.hairlineWidth,
586
- borderTopColor: TOOLBAR_BORDER,
587
- paddingVertical: TOOLBAR_PADDING_V,
588
- overflow: 'hidden',
589
- },
590
- containerWithoutTopBorder: {
591
- borderTopWidth: 0,
592
- },
593
- scrollContent: {
594
- flexDirection: 'row',
595
- alignItems: 'center',
596
- paddingHorizontal: TOOLBAR_PADDING_H,
597
- minWidth: '100%',
598
- },
599
- button: {
600
- width: BUTTON_HIT,
601
- height: BUTTON_VISIBLE,
602
- justifyContent: 'center',
603
- alignItems: 'center',
604
- borderRadius: BUTTON_RADIUS,
605
- },
606
- separator: {
607
- width: StyleSheet.hairlineWidth,
608
- height: 20,
609
- marginHorizontal: 4,
610
- backgroundColor: SEPARATOR_COLOR,
611
- },
612
- iconContainer: {
613
- justifyContent: 'center',
614
- alignItems: 'center',
615
- },
616
- iconText: {
617
- fontSize: 16,
618
- fontWeight: '600',
619
- },
620
- });