@apollohg/react-native-prose-editor 0.2.0 → 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 (32) hide show
  1. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +228 -2
  2. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  3. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +4 -0
  4. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +3 -0
  5. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +6 -0
  6. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +347 -10
  7. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +76 -8
  8. package/dist/EditorToolbar.d.ts +9 -2
  9. package/dist/EditorToolbar.js +20 -10
  10. package/dist/NativeEditorBridge.d.ts +2 -0
  11. package/dist/NativeEditorBridge.js +3 -0
  12. package/dist/NativeRichTextEditor.d.ts +17 -1
  13. package/dist/NativeRichTextEditor.js +94 -37
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +5 -1
  16. package/dist/schemas.d.ts +12 -0
  17. package/dist/schemas.js +45 -1
  18. package/ios/EditorCore.xcframework/Info.plist +5 -5
  19. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  20. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  21. package/ios/EditorLayoutManager.swift +0 -16
  22. package/ios/Generated_editor_core.swift +20 -2
  23. package/ios/NativeEditorExpoView.swift +51 -16
  24. package/ios/NativeEditorModule.swift +3 -0
  25. package/ios/RenderBridge.swift +208 -0
  26. package/ios/RichTextEditorView.swift +896 -15
  27. package/ios/editor_coreFFI/editor_coreFFI.h +11 -0
  28. package/package.json +1 -1
  29. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  30. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  31. package/rust/android/x86_64/libeditor_core.so +0 -0
  32. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +25 -2
@@ -65,6 +65,7 @@ const DEFAULT_GLYPH_ICONS = {
65
65
  underline: 'U',
66
66
  strike: 'S',
67
67
  link: '🔗',
68
+ image: '🖼',
68
69
  blockquote: '❝',
69
70
  bulletList: '•≡',
70
71
  orderedList: '1.',
@@ -81,6 +82,7 @@ const DEFAULT_MATERIAL_ICONS = {
81
82
  underline: 'format-underlined',
82
83
  strike: 'strikethrough-s',
83
84
  link: 'link',
85
+ image: 'image',
84
86
  blockquote: 'format-quote',
85
87
  bulletList: 'format-list-bulleted',
86
88
  orderedList: 'format-list-numbered',
@@ -91,7 +93,7 @@ const DEFAULT_MATERIAL_ICONS = {
91
93
  undo: 'undo',
92
94
  redo: 'redo',
93
95
  };
94
- function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder = true, }) {
96
+ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder = true, }) {
95
97
  const marks = activeState.marks ?? {};
96
98
  const nodes = activeState.nodes ?? {};
97
99
  const commands = activeState.commands ?? {};
@@ -130,6 +132,8 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
130
132
  : (onToggleOrderedList ?? null);
131
133
  case 'link':
132
134
  return onRequestLink ?? null;
135
+ case 'image':
136
+ return onRequestImage ?? null;
133
137
  case 'blockquote':
134
138
  return onToggleBlockquote ?? null;
135
139
  case 'node':
@@ -170,6 +174,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
170
174
  onRedo,
171
175
  onRunCommand,
172
176
  onRequestLink,
177
+ onRequestImage,
173
178
  onToolbarAction,
174
179
  onToggleBold,
175
180
  onToggleBlockquote,
@@ -187,15 +192,17 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
187
192
  ? `mark:${item.mark}:${index}`
188
193
  : item.type === 'link'
189
194
  ? `link:${index}`
190
- : item.type === 'blockquote'
191
- ? `blockquote:${index}`
192
- : item.type === 'list'
193
- ? `list:${item.listType}:${index}`
194
- : item.type === 'command'
195
- ? `command:${item.command}:${index}`
196
- : item.type === 'node'
197
- ? `node:${item.nodeType}:${index}`
198
- : `action:${item.key}:${index}`), []);
195
+ : item.type === 'image'
196
+ ? `image:${index}`
197
+ : item.type === 'blockquote'
198
+ ? `blockquote:${index}`
199
+ : item.type === 'list'
200
+ ? `list:${item.listType}:${index}`
201
+ : item.type === 'command'
202
+ ? `command:${item.command}:${index}`
203
+ : item.type === 'node'
204
+ ? `node:${item.nodeType}:${index}`
205
+ : `action:${item.key}:${index}`), []);
199
206
  const renderedItems = [];
200
207
  for (let index = 0; index < toolbarItems.length; index += 1) {
201
208
  const item = toolbarItems[index];
@@ -221,6 +228,9 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
221
228
  isActive = isMarkActive('link');
222
229
  isDisabled = !allowedMarks.includes('link') || !onRequestLink;
223
230
  break;
231
+ case 'image':
232
+ isDisabled = !insertableNodes.includes('image') || !onRequestImage;
233
+ break;
224
234
  case 'blockquote':
225
235
  isActive = !!nodes['blockquote'];
226
236
  isDisabled = !commands['toggleBlockquote'];
@@ -87,6 +87,7 @@ export interface RenderElement {
87
87
  depth?: number;
88
88
  docPos?: number;
89
89
  label?: string;
90
+ attrs?: Record<string, unknown>;
90
91
  listContext?: ListContext;
91
92
  }
92
93
  export interface ActiveState {
@@ -139,6 +140,7 @@ export declare class NativeEditorBridge {
139
140
  static create(config?: {
140
141
  maxLength?: number;
141
142
  schemaJson?: string;
143
+ allowBase64Images?: boolean;
142
144
  }): NativeEditorBridge;
143
145
  /** The underlying native editor ID. */
144
146
  get editorId(): number;
@@ -227,6 +227,9 @@ class NativeEditorBridge {
227
227
  const configObj = {};
228
228
  if (config?.maxLength != null)
229
229
  configObj.maxLength = config.maxLength;
230
+ if (config?.allowBase64Images != null) {
231
+ configObj.allowBase64Images = config.allowBase64Images;
232
+ }
230
233
  if (config?.schemaJson != null) {
231
234
  try {
232
235
  configObj.schema = JSON.parse(config.schemaJson);
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
2
  import { type StyleProp, type ViewStyle } from 'react-native';
3
- import { type ActiveState, type DocumentJSON, type Selection } from './NativeEditorBridge';
3
+ import { type ActiveState, type DocumentJSON, type HistoryState, type Selection } from './NativeEditorBridge';
4
4
  import { type EditorToolbarItem } from './EditorToolbar';
5
5
  import { type EditorTheme } from './EditorTheme';
6
6
  import { type EditorAddons } from './addons';
7
7
  import { type SchemaDefinition } from './schemas';
8
+ import { type ImageNodeAttributes } from './schemas';
8
9
  export type NativeRichTextEditorHeightBehavior = 'fixed' | 'autoGrow';
9
10
  export type NativeRichTextEditorToolbarPlacement = 'keyboard' | 'inline';
10
11
  export interface RemoteSelectionDecoration {
@@ -23,6 +24,11 @@ export interface LinkRequestContext {
23
24
  setLink: (href: string) => void;
24
25
  unsetLink: () => void;
25
26
  }
27
+ export interface ImageRequestContext {
28
+ selection: Selection;
29
+ allowBase64: boolean;
30
+ insertImage: (src: string, attrs?: Omit<ImageNodeAttributes, 'src'>) => void;
31
+ }
26
32
  export interface NativeRichTextEditorProps {
27
33
  /** Initial content as HTML (uncontrolled mode). */
28
34
  initialContent?: string;
@@ -54,6 +60,12 @@ export interface NativeRichTextEditorProps {
54
60
  onToolbarAction?: (key: string) => void;
55
61
  /** Called when a toolbar link item is pressed so the host can collect/edit a URL. */
56
62
  onRequestLink?: (context: LinkRequestContext) => void;
63
+ /** Called when a toolbar image item is pressed so the host can choose an image source. */
64
+ onRequestImage?: (context: ImageRequestContext) => void;
65
+ /** Whether `data:image/...` sources are accepted for image insertion and HTML parsing. */
66
+ allowBase64Images?: boolean;
67
+ /** Whether selected images show native resize handles. */
68
+ allowImageResizing?: boolean;
57
69
  /** Called when content changes with the current HTML. */
58
70
  onContentChange?: (html: string) => void;
59
71
  /** Called when content changes with the current ProseMirror JSON. */
@@ -62,6 +74,8 @@ export interface NativeRichTextEditorProps {
62
74
  onSelectionChange?: (selection: Selection) => void;
63
75
  /** Called when active formatting state changes. */
64
76
  onActiveStateChange?: (state: ActiveState) => void;
77
+ /** Called when undo/redo availability changes. */
78
+ onHistoryStateChange?: (state: HistoryState) => void;
65
79
  /** Called when the editor gains focus. */
66
80
  onFocus?: () => void;
67
81
  /** Called when the editor loses focus. */
@@ -98,6 +112,8 @@ export interface NativeRichTextEditorRef {
98
112
  outdentListItem(): void;
99
113
  /** Insert a void node (e.g. 'horizontalRule'). */
100
114
  insertNode(nodeType: string): void;
115
+ /** Insert a block image node with the given source and optional metadata. */
116
+ insertImage(src: string, attrs?: Omit<ImageNodeAttributes, 'src'>): void;
101
117
  /** Insert text at the current cursor position. */
102
118
  insertText(text: string): void;
103
119
  /** Insert HTML content at the current selection. */
@@ -10,11 +10,16 @@ const EditorToolbar_1 = require("./EditorToolbar");
10
10
  const EditorTheme_1 = require("./EditorTheme");
11
11
  const addons_1 = require("./addons");
12
12
  const schemas_1 = require("./schemas");
13
+ const schemas_2 = require("./schemas");
13
14
  const NativeEditorView = (0, expo_modules_core_1.requireNativeViewManager)('NativeEditor');
14
15
  const DEV_NATIVE_VIEW_KEY = __DEV__
15
16
  ? `native-editor-dev:${Math.random().toString(36).slice(2)}`
16
17
  : 'native-editor';
17
18
  const LINK_TOOLBAR_ACTION_KEY = '__native-editor-link__';
19
+ const IMAGE_TOOLBAR_ACTION_KEY = '__native-editor-image__';
20
+ function isImageDataUrl(value) {
21
+ return /^data:image\//i.test(value.trim());
22
+ }
18
23
  function isPromiseLike(value) {
19
24
  return (value != null &&
20
25
  typeof value === 'object' &&
@@ -59,7 +64,7 @@ function serializeRemoteSelections(remoteSelections) {
59
64
  }
60
65
  return JSON.stringify(remoteSelections);
61
66
  }
62
- exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, schema, placeholder, editable = true, maxLength, autoFocus = false, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, }, ref) {
67
+ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, schema, placeholder, editable = true, maxLength, autoFocus = false, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
63
68
  const bridgeRef = (0, react_1.useRef)(null);
64
69
  const nativeViewRef = (0, react_1.useRef)(null);
65
70
  const [isReady, setIsReady] = (0, react_1.useState)(false);
@@ -97,6 +102,8 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
97
102
  onSelectionChangeRef.current = onSelectionChange;
98
103
  const onActiveStateChangeRef = (0, react_1.useRef)(onActiveStateChange);
99
104
  onActiveStateChangeRef.current = onActiveStateChange;
105
+ const onHistoryStateChangeRef = (0, react_1.useRef)(onHistoryStateChange);
106
+ onHistoryStateChangeRef.current = onHistoryStateChange;
100
107
  const onFocusRef = (0, react_1.useRef)(onFocus);
101
108
  onFocusRef.current = onFocus;
102
109
  const onBlurRef = (0, react_1.useRef)(onBlur);
@@ -171,6 +178,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
171
178
  }
172
179
  syncStateFromUpdate(update);
173
180
  onActiveStateChangeRef.current?.(update.activeState);
181
+ onHistoryStateChangeRef.current?.(update.historyState);
174
182
  if (!options?.suppressContentCallbacks) {
175
183
  if (onContentChangeRef.current && bridgeRef.current) {
176
184
  onContentChangeRef.current(bridgeRef.current.getHtml());
@@ -187,7 +195,10 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
187
195
  ? (0, addons_1.withMentionsSchema)(schema ?? schemas_1.tiptapSchema)
188
196
  : schema;
189
197
  const schemaJson = effectiveSchema ? JSON.stringify(effectiveSchema) : undefined;
190
- const bridge = NativeEditorBridge_1.NativeEditorBridge.create(maxLength != null || schemaJson ? { maxLength, schemaJson } : undefined);
198
+ const bridgeConfig = maxLength != null || schemaJson || allowBase64Images
199
+ ? { maxLength, schemaJson, allowBase64Images }
200
+ : undefined;
201
+ const bridge = NativeEditorBridge_1.NativeEditorBridge.create(bridgeConfig);
191
202
  bridgeRef.current = bridge;
192
203
  setEditorInstanceId(bridge.editorId);
193
204
  // Four-way content initialization: value > valueJSON > initialJSON > initialContent
@@ -213,7 +224,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
213
224
  setIsReady(false);
214
225
  };
215
226
  // eslint-disable-next-line react-hooks/exhaustive-deps
216
- }, [schema, maxLength, syncStateFromUpdate, Boolean(addons?.mentions)]);
227
+ }, [schema, maxLength, syncStateFromUpdate, Boolean(addons?.mentions), allowBase64Images]);
217
228
  (0, react_1.useEffect)(() => {
218
229
  if (value == null)
219
230
  return;
@@ -280,6 +291,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
280
291
  return;
281
292
  syncStateFromUpdate(update);
282
293
  onActiveStateChangeRef.current?.(update.activeState);
294
+ onHistoryStateChangeRef.current?.(update.historyState);
283
295
  if (onContentChangeRef.current) {
284
296
  onContentChangeRef.current(bridgeRef.current.getHtml());
285
297
  }
@@ -312,6 +324,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
312
324
  selectionRef.current = nextSelection;
313
325
  if (currentState) {
314
326
  onActiveStateChangeRef.current?.(currentState.activeState);
327
+ onHistoryStateChangeRef.current?.(currentState.historyState);
315
328
  }
316
329
  onSelectionChangeRef.current?.(nextSelection);
317
330
  }, [syncStateFromUpdate]);
@@ -334,25 +347,42 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
334
347
  return;
335
348
  setAutoGrowHeight((prev) => (prev === nextHeight ? prev : nextHeight));
336
349
  }, [autoGrowHeight, heightBehavior]);
337
- const openLinkRequest = (0, react_1.useCallback)(() => {
338
- const requestSelection = selectionRef.current;
339
- const restoreCapturedSelection = () => {
340
- if (requestSelection.type === 'text') {
341
- const { anchor, head } = requestSelection;
342
- if (anchor == null || head == null) {
343
- return;
344
- }
345
- bridgeRef.current?.setSelection(anchor, head);
350
+ const restoreSelection = (0, react_1.useCallback)((selection) => {
351
+ if (selection.type === 'text') {
352
+ const { anchor, head } = selection;
353
+ if (anchor == null || head == null) {
346
354
  return;
347
355
  }
348
- if (requestSelection.type === 'node') {
349
- const { pos } = requestSelection;
350
- if (pos == null) {
351
- return;
352
- }
353
- bridgeRef.current?.setSelection(pos, pos);
356
+ bridgeRef.current?.setSelection(anchor, head);
357
+ return;
358
+ }
359
+ if (selection.type === 'node') {
360
+ const { pos } = selection;
361
+ if (pos == null) {
362
+ return;
354
363
  }
355
- };
364
+ bridgeRef.current?.setSelection(pos, pos);
365
+ }
366
+ }, []);
367
+ const insertImage = (0, react_1.useCallback)((src, attrs, selection) => {
368
+ const trimmedSrc = src.trim();
369
+ if (!trimmedSrc)
370
+ return;
371
+ if (!allowBase64Images && isImageDataUrl(trimmedSrc)) {
372
+ return;
373
+ }
374
+ runAndApply(() => {
375
+ if (selection) {
376
+ restoreSelection(selection);
377
+ }
378
+ return (bridgeRef.current?.insertContentJson((0, schemas_2.buildImageFragmentJson)({
379
+ src: trimmedSrc,
380
+ ...(attrs ?? {}),
381
+ })) ?? null);
382
+ });
383
+ }, [allowBase64Images, restoreSelection, runAndApply]);
384
+ const openLinkRequest = (0, react_1.useCallback)(() => {
385
+ const requestSelection = selectionRef.current;
356
386
  onRequestLink?.({
357
387
  href: currentLinkHref,
358
388
  isActive: activeState.marks.link === true,
@@ -362,7 +392,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
362
392
  if (!trimmedHref)
363
393
  return;
364
394
  runAndApply(() => {
365
- restoreCapturedSelection();
395
+ restoreSelection(requestSelection);
366
396
  return (bridgeRef.current?.setMark('link', {
367
397
  href: trimmedHref,
368
398
  }) ?? null);
@@ -370,19 +400,31 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
370
400
  },
371
401
  unsetLink: () => {
372
402
  runAndApply(() => {
373
- restoreCapturedSelection();
403
+ restoreSelection(requestSelection);
374
404
  return bridgeRef.current?.unsetMark('link') ?? null;
375
405
  }, { skipNativeApplyIfContentUnchanged: true });
376
406
  },
377
407
  });
378
- }, [activeState.marks.link, currentLinkHref, onRequestLink, runAndApply]);
408
+ }, [activeState.marks.link, currentLinkHref, onRequestLink, restoreSelection, runAndApply]);
409
+ const openImageRequest = (0, react_1.useCallback)(() => {
410
+ const requestSelection = selectionRef.current;
411
+ onRequestImage?.({
412
+ selection: requestSelection,
413
+ allowBase64: allowBase64Images,
414
+ insertImage: (src, attrs) => insertImage(src, attrs, requestSelection),
415
+ });
416
+ }, [allowBase64Images, insertImage, onRequestImage]);
379
417
  const handleToolbarAction = (0, react_1.useCallback)((event) => {
380
418
  if (event.nativeEvent.key === LINK_TOOLBAR_ACTION_KEY) {
381
419
  openLinkRequest();
382
420
  return;
383
421
  }
422
+ if (event.nativeEvent.key === IMAGE_TOOLBAR_ACTION_KEY) {
423
+ openImageRequest();
424
+ return;
425
+ }
384
426
  onToolbarAction?.(event.nativeEvent.key);
385
- }, [onToolbarAction, openLinkRequest]);
427
+ }, [onToolbarAction, openImageRequest, openLinkRequest]);
386
428
  const handleAddonEvent = (0, react_1.useCallback)((event) => {
387
429
  let parsed = null;
388
430
  try {
@@ -451,6 +493,9 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
451
493
  insertNode(nodeType) {
452
494
  runAndApply(() => bridgeRef.current?.insertNode(nodeType) ?? null);
453
495
  },
496
+ insertImage(src, attrs) {
497
+ insertImage(src, attrs);
498
+ },
454
499
  insertText(text) {
455
500
  runAndApply(() => bridgeRef.current?.replaceSelectionText(text) ?? null);
456
501
  },
@@ -497,21 +542,33 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
497
542
  return false;
498
543
  return bridgeRef.current.canRedo();
499
544
  },
500
- }), [runAndApply]);
545
+ }), [insertImage, runAndApply]);
501
546
  if (!isReady)
502
547
  return null;
503
548
  const toolbarItemsForNative = toolbarItems.map((item) => {
504
- if (item.type !== 'link') {
505
- return item;
506
- }
507
- return {
508
- type: 'action',
509
- key: LINK_TOOLBAR_ACTION_KEY,
510
- label: item.label,
511
- icon: item.icon,
512
- isActive: activeState.marks.link === true,
513
- isDisabled: !editable || !onRequestLink || !activeState.allowedMarks.includes('link'),
514
- };
549
+ if (item.type === 'link') {
550
+ return {
551
+ type: 'action',
552
+ key: LINK_TOOLBAR_ACTION_KEY,
553
+ label: item.label,
554
+ icon: item.icon,
555
+ isActive: activeState.marks.link === true,
556
+ isDisabled: !editable || !onRequestLink || !activeState.allowedMarks.includes('link'),
557
+ };
558
+ }
559
+ if (item.type === 'image') {
560
+ return {
561
+ type: 'action',
562
+ key: IMAGE_TOOLBAR_ACTION_KEY,
563
+ label: item.label,
564
+ icon: item.icon,
565
+ isActive: false,
566
+ isDisabled: !editable
567
+ || !onRequestImage
568
+ || !activeState.insertableNodes.includes(schemas_2.IMAGE_NODE_NAME),
569
+ };
570
+ }
571
+ return item;
515
572
  });
516
573
  const themeJson = (0, EditorTheme_1.serializeEditorTheme)(theme);
517
574
  const addonsJson = (0, addons_1.serializeEditorAddons)(addons);
@@ -568,7 +625,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
568
625
  runAndApply(() => bridgeRef.current?.redo() ?? null);
569
626
  break;
570
627
  }
571
- }, onRequestLink: openLinkRequest, onToolbarAction: onToolbarAction, onToggleBold: () => runAndApply(() => bridgeRef.current?.toggleMark('bold') ?? null, {
628
+ }, onRequestLink: openLinkRequest, onRequestImage: openImageRequest, onToolbarAction: onToolbarAction, onToggleBold: () => runAndApply(() => bridgeRef.current?.toggleMark('bold') ?? null, {
572
629
  skipNativeApplyIfContentUnchanged: true,
573
630
  }), onToggleItalic: () => runAndApply(() => bridgeRef.current?.toggleMark('italic') ?? null, {
574
631
  skipNativeApplyIfContentUnchanged: true,
@@ -577,7 +634,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
577
634
  }), onToggleStrike: () => runAndApply(() => bridgeRef.current?.toggleMark('strike') ?? null, {
578
635
  skipNativeApplyIfContentUnchanged: true,
579
636
  }), onToggleBulletList: () => runAndApply(() => bridgeRef.current?.toggleList('bulletList') ?? null), onToggleOrderedList: () => runAndApply(() => bridgeRef.current?.toggleList('orderedList') ?? null), onIndentList: () => runAndApply(() => bridgeRef.current?.indentListItem() ?? null), onOutdentList: () => runAndApply(() => bridgeRef.current?.outdentListItem() ?? null), onInsertHorizontalRule: () => runAndApply(() => bridgeRef.current?.insertNode('horizontalRule') ?? null), onInsertLineBreak: () => runAndApply(() => bridgeRef.current?.insertNode('hardBreak') ?? null), onUndo: () => runAndApply(() => bridgeRef.current?.undo() ?? null), onRedo: () => runAndApply(() => bridgeRef.current?.redo() ?? null) }) }));
580
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson: toolbarPlacement === 'inline' && isFocused ? toolbarFrameJson : undefined, editorUpdateJson: pendingNativeUpdate.json, editorUpdateRevision: pendingNativeUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
637
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson: toolbarPlacement === 'inline' && isFocused ? toolbarFrameJson : undefined, editorUpdateJson: pendingNativeUpdate.json, editorUpdateRevision: pendingNativeUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
581
638
  });
582
639
  const styles = react_native_1.StyleSheet.create({
583
640
  container: {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, } from './NativeRichTextEditor';
1
+ export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
2
2
  export { EditorToolbar, DEFAULT_EDITOR_TOOLBAR_ITEMS, type EditorToolbarProps, type EditorToolbarItem, type EditorToolbarIcon, type EditorToolbarDefaultIconId, type EditorToolbarSFSymbolIcon, type EditorToolbarMaterialIcon, type EditorToolbarCommand, type EditorToolbarListType, } from './EditorToolbar';
3
3
  export type { EditorContentInsets, EditorTheme, EditorTextStyle, EditorHeadingTheme, EditorListTheme, EditorHorizontalRuleTheme, EditorMentionTheme, EditorToolbarTheme, EditorToolbarAppearance, EditorFontStyle, EditorFontWeight, } from './EditorTheme';
4
4
  export { MENTION_NODE_NAME, mentionNodeSpec, withMentionsSchema, buildMentionFragmentJson, type EditorAddons, type MentionsAddonConfig, type MentionSuggestion, type MentionQueryChangeEvent, type MentionSelectEvent, type EditorAddonEvent, } from './addons';
5
- export { tiptapSchema, prosemirrorSchema, type SchemaDefinition, type NodeSpec, type MarkSpec, type AttrSpec, } from './schemas';
5
+ export { tiptapSchema, prosemirrorSchema, IMAGE_NODE_NAME, imageNodeSpec, withImagesSchema, buildImageFragmentJson, type SchemaDefinition, type NodeSpec, type MarkSpec, type AttrSpec, type ImageNodeAttributes, } from './schemas';
6
6
  export { createYjsCollaborationController, useYjsCollaboration, type YjsCollaborationOptions, type YjsCollaborationState, type YjsTransportStatus, type LocalAwarenessState, type LocalAwarenessUser, type UseYjsCollaborationResult, type YjsCollaborationController, } from './YjsCollaboration';
7
7
  export type { Selection, ActiveState, HistoryState, EditorUpdate, DocumentJSON, CollaborationPeer, EncodedCollaborationStateInput, } from './NativeEditorBridge';
8
8
  export { encodeCollaborationStateBase64, decodeCollaborationStateBase64, } from './NativeEditorBridge';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.decodeCollaborationStateBase64 = exports.encodeCollaborationStateBase64 = exports.useYjsCollaboration = exports.createYjsCollaborationController = exports.prosemirrorSchema = exports.tiptapSchema = exports.buildMentionFragmentJson = exports.withMentionsSchema = exports.mentionNodeSpec = exports.MENTION_NODE_NAME = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = exports.EditorToolbar = exports.NativeRichTextEditor = void 0;
3
+ exports.decodeCollaborationStateBase64 = exports.encodeCollaborationStateBase64 = exports.useYjsCollaboration = exports.createYjsCollaborationController = exports.buildImageFragmentJson = exports.withImagesSchema = exports.imageNodeSpec = exports.IMAGE_NODE_NAME = exports.prosemirrorSchema = exports.tiptapSchema = exports.buildMentionFragmentJson = exports.withMentionsSchema = exports.mentionNodeSpec = exports.MENTION_NODE_NAME = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = exports.EditorToolbar = exports.NativeRichTextEditor = void 0;
4
4
  var NativeRichTextEditor_1 = require("./NativeRichTextEditor");
5
5
  Object.defineProperty(exports, "NativeRichTextEditor", { enumerable: true, get: function () { return NativeRichTextEditor_1.NativeRichTextEditor; } });
6
6
  var EditorToolbar_1 = require("./EditorToolbar");
@@ -14,6 +14,10 @@ Object.defineProperty(exports, "buildMentionFragmentJson", { enumerable: true, g
14
14
  var schemas_1 = require("./schemas");
15
15
  Object.defineProperty(exports, "tiptapSchema", { enumerable: true, get: function () { return schemas_1.tiptapSchema; } });
16
16
  Object.defineProperty(exports, "prosemirrorSchema", { enumerable: true, get: function () { return schemas_1.prosemirrorSchema; } });
17
+ Object.defineProperty(exports, "IMAGE_NODE_NAME", { enumerable: true, get: function () { return schemas_1.IMAGE_NODE_NAME; } });
18
+ Object.defineProperty(exports, "imageNodeSpec", { enumerable: true, get: function () { return schemas_1.imageNodeSpec; } });
19
+ Object.defineProperty(exports, "withImagesSchema", { enumerable: true, get: function () { return schemas_1.withImagesSchema; } });
20
+ Object.defineProperty(exports, "buildImageFragmentJson", { enumerable: true, get: function () { return schemas_1.buildImageFragmentJson; } });
17
21
  var YjsCollaboration_1 = require("./YjsCollaboration");
18
22
  Object.defineProperty(exports, "createYjsCollaborationController", { enumerable: true, get: function () { return YjsCollaboration_1.createYjsCollaborationController; } });
19
23
  Object.defineProperty(exports, "useYjsCollaboration", { enumerable: true, get: function () { return YjsCollaboration_1.useYjsCollaboration; } });
package/dist/schemas.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { DocumentJSON } from './NativeEditorBridge';
1
2
  export interface AttrSpec {
2
3
  default?: unknown;
3
4
  }
@@ -19,5 +20,16 @@ export interface SchemaDefinition {
19
20
  nodes: NodeSpec[];
20
21
  marks: MarkSpec[];
21
22
  }
23
+ export interface ImageNodeAttributes {
24
+ src: string;
25
+ alt?: string | null;
26
+ title?: string | null;
27
+ width?: number | null;
28
+ height?: number | null;
29
+ }
30
+ export declare const IMAGE_NODE_NAME = "image";
31
+ export declare function imageNodeSpec(name?: string): NodeSpec;
32
+ export declare function withImagesSchema(schema: SchemaDefinition): SchemaDefinition;
33
+ export declare function buildImageFragmentJson(attrs: ImageNodeAttributes): DocumentJSON;
22
34
  export declare const tiptapSchema: SchemaDefinition;
23
35
  export declare const prosemirrorSchema: SchemaDefinition;
package/dist/schemas.js CHANGED
@@ -1,6 +1,48 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.prosemirrorSchema = exports.tiptapSchema = void 0;
3
+ exports.prosemirrorSchema = exports.tiptapSchema = exports.IMAGE_NODE_NAME = void 0;
4
+ exports.imageNodeSpec = imageNodeSpec;
5
+ exports.withImagesSchema = withImagesSchema;
6
+ exports.buildImageFragmentJson = buildImageFragmentJson;
7
+ exports.IMAGE_NODE_NAME = 'image';
8
+ function imageNodeSpec(name = exports.IMAGE_NODE_NAME) {
9
+ return {
10
+ name,
11
+ content: '',
12
+ group: 'block',
13
+ attrs: {
14
+ src: {},
15
+ alt: { default: null },
16
+ title: { default: null },
17
+ width: { default: null },
18
+ height: { default: null },
19
+ },
20
+ role: 'block',
21
+ htmlTag: 'img',
22
+ isVoid: true,
23
+ };
24
+ }
25
+ function withImagesSchema(schema) {
26
+ const hasImageNode = schema.nodes.some((node) => node.name === exports.IMAGE_NODE_NAME);
27
+ if (hasImageNode) {
28
+ return schema;
29
+ }
30
+ return {
31
+ ...schema,
32
+ nodes: [...schema.nodes, imageNodeSpec()],
33
+ };
34
+ }
35
+ function buildImageFragmentJson(attrs) {
36
+ return {
37
+ type: 'doc',
38
+ content: [
39
+ {
40
+ type: exports.IMAGE_NODE_NAME,
41
+ attrs,
42
+ },
43
+ ],
44
+ };
45
+ }
4
46
  const MARKS = [
5
47
  { name: 'bold' },
6
48
  { name: 'italic' },
@@ -66,6 +108,7 @@ exports.tiptapSchema = {
66
108
  htmlTag: 'hr',
67
109
  isVoid: true,
68
110
  },
111
+ imageNodeSpec(),
69
112
  {
70
113
  name: 'text',
71
114
  content: '',
@@ -133,6 +176,7 @@ exports.prosemirrorSchema = {
133
176
  htmlTag: 'hr',
134
177
  isVoid: true,
135
178
  },
179
+ imageNodeSpec('image'),
136
180
  {
137
181
  name: 'text',
138
182
  content: '',
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>libeditor_core.a</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64_x86_64-simulator</string>
11
+ <string>ios-arm64</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>libeditor_core.a</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
- <string>x86_64</string>
18
17
  </array>
19
18
  <key>SupportedPlatform</key>
20
19
  <string>ios</string>
21
- <key>SupportedPlatformVariant</key>
22
- <string>simulator</string>
23
20
  </dict>
24
21
  <dict>
25
22
  <key>BinaryPath</key>
26
23
  <string>libeditor_core.a</string>
27
24
  <key>LibraryIdentifier</key>
28
- <string>ios-arm64</string>
25
+ <string>ios-arm64_x86_64-simulator</string>
29
26
  <key>LibraryPath</key>
30
27
  <string>libeditor_core.a</string>
31
28
  <key>SupportedArchitectures</key>
32
29
  <array>
33
30
  <string>arm64</string>
31
+ <string>x86_64</string>
34
32
  </array>
35
33
  <key>SupportedPlatform</key>
36
34
  <string>ios</string>
35
+ <key>SupportedPlatformVariant</key>
36
+ <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
@@ -1,17 +1,10 @@
1
1
  import UIKit
2
2
  import CoreText
3
- import os
4
3
 
5
4
  /// Draws list markers visually in the gutter without inserting them into the
6
5
  /// editable text storage. This keeps UIKit paragraph-start behaviors, such as
7
6
  /// sentence auto-capitalization, working naturally inside list items.
8
7
  final class EditorLayoutManager: NSLayoutManager {
9
- private static let blockquoteLog = Logger(
10
- subsystem: "com.apollohg.prose-editor",
11
- category: "blockquote"
12
- )
13
-
14
- private var blockquoteDrawPassCounter: UInt64 = 0
15
8
  private(set) var blockquoteStripeDrawPassesForTesting: [[CGRect]] = []
16
9
 
17
10
  func blockquoteStripeRectsForTesting(
@@ -66,9 +59,6 @@ final class EditorLayoutManager: NSLayoutManager {
66
59
 
67
60
  guard let textStorage, glyphsToShow.length > 0 else { return }
68
61
 
69
- blockquoteDrawPassCounter &+= 1
70
- let drawPass = blockquoteDrawPassCounter
71
-
72
62
  let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
73
63
  let nsString = textStorage.string as NSString
74
64
  var drawnParagraphStarts = Set<Int>()
@@ -126,9 +116,6 @@ final class EditorLayoutManager: NSLayoutManager {
126
116
  stripeRect: stripeRect,
127
117
  color: color
128
118
  )
129
- Self.blockquoteLog.notice(
130
- "[drawGlyphs] pass=\(drawPass, privacy: .public) glyphRange=\(glyphsToShow.location, privacy: .public)..<\(glyphsToShow.location + glyphsToShow.length, privacy: .public) groupRange=\(groupRange.location, privacy: .public)..<\(groupRange.location + groupRange.length, privacy: .public) stripe=(x:\(stripeRect.minX, privacy: .public), y:\(stripeRect.minY, privacy: .public), w:\(stripeRect.width, privacy: .public), h:\(stripeRect.height, privacy: .public))"
131
- )
132
119
  drawnStripeRects.append(stripeRect)
133
120
  }
134
121
 
@@ -259,9 +246,6 @@ final class EditorLayoutManager: NSLayoutManager {
259
246
  width: borderWidth,
260
247
  height: bottomEdge - topEdge
261
248
  )
262
- Self.blockquoteLog.notice(
263
- "[stripeRect] chars=\(characterRange.location, privacy: .public)..<\(characterRange.location + characterRange.length, privacy: .public) textLeading=\(textLeadingEdge, privacy: .public) verticalEdges=(top:\(topEdge, privacy: .public), bottom:\(bottomEdge, privacy: .public)) stripe=(x:\(stripeRect.minX, privacy: .public), y:\(stripeRect.minY, privacy: .public), w:\(stripeRect.width, privacy: .public), h:\(stripeRect.height, privacy: .public)) borderWidth=\(borderWidth, privacy: .public) gap=\(gap, privacy: .public)"
264
- )
265
249
  return stripeRect
266
250
  }
267
251