@apollohg/react-native-rich-text-editor 2.0.2 → 2.0.4

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 (43) hide show
  1. package/README.md +21 -7
  2. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +20 -6
  3. package/android/src/main/java/com/apollohg/editor/EditorEditTextClipboard.kt +181 -36
  4. package/android/src/main/java/com/apollohg/editor/EditorEditTextSelection.kt +7 -0
  5. package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceInteraction.kt +31 -11
  6. package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +36 -0
  7. package/android/src/main/java/com/apollohg/editor/EditorV2Backend.kt +1 -0
  8. package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +10 -0
  9. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +2 -0
  10. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewProperties.kt +4 -0
  11. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +3 -0
  12. package/android/src/main/java/com/apollohg/editor/UniffiEditorV2Backend.kt +4 -0
  13. package/dist/NativeRichTextEditor.d.ts +1 -1
  14. package/dist/RichTextEditorNativeTypes.d.ts +2 -1
  15. package/dist/RichTextEditorTypes.d.ts +4 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/useRichTextEditorPresentation.d.ts +1 -1
  18. package/dist/useRichTextEditorPresentation.js +2 -2
  19. package/dist/useRichTextEditorState.d.ts +5 -3
  20. package/dist/useRichTextEditorState.js +4 -3
  21. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  22. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  23. package/ios/EditorTextView+KeyboardInput.swift +299 -0
  24. package/ios/EditorTextView+Selection.swift +5 -2
  25. package/ios/EditorTextView.swift +63 -43
  26. package/ios/EditorV2Adapter+Commands.swift +22 -0
  27. package/ios/EditorV2Shadow.swift +12 -0
  28. package/ios/Generated_editor_core.swift +10 -0
  29. package/ios/NativeEditorExpoView+Properties.swift +4 -0
  30. package/ios/NativeEditorModule.swift +3 -0
  31. package/ios/editor_coreFFI/editor_coreFFI.h +11 -0
  32. package/package.json +1 -1
  33. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  34. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  35. package/rust/android/x86/libeditor_core.so +0 -0
  36. package/rust/android/x86_64/libeditor_core.so +0 -0
  37. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +18 -0
  38. package/src/NativeRichTextEditor.tsx +1 -0
  39. package/src/RichTextEditorNativeTypes.tsx +2 -0
  40. package/src/RichTextEditorTypes.tsx +5 -0
  41. package/src/index.ts +1 -0
  42. package/src/useRichTextEditorPresentation.tsx +3 -0
  43. package/src/useRichTextEditorState.tsx +5 -2
@@ -2,4 +2,4 @@ import { type useRichTextEditorState } from './useRichTextEditorState';
2
2
  import { type useRichTextEditorMentions } from './useRichTextEditorMentions';
3
3
  import { type useRichTextEditorCommands } from './useRichTextEditorCommands';
4
4
  import { type useRichTextEditorEvents } from './useRichTextEditorEvents';
5
- export declare function useRichTextEditorPresentation(context: Pick<ReturnType<typeof useRichTextEditorState>, 'theme' | 'addons' | 'imageLoadingPolicy' | 'androidInputOptions' | 'remoteSelections' | 'atoms' | 'atomState' | 'atomComponents' | 'warnedUnknownAtomTypesRef' | 'atomContentWidth' | 'atomPositions' | 'atomViewport' | 'virtualizeAtoms' | 'nativeAtomViewport' | 'selectedKeys' | 'editable' | 'atomsInteractive' | 'documentHandle' | 'activeState' | 'onRequestLink' | 'onRequestImage' | 'toolbarItemsSerializationCacheRef' | 'toolbarItems' | 'document' | 'toolbarPlacement' | 'showToolbar' | 'containerStyle' | 'style' | 'heightBehavior' | 'autoGrowHeight' | 'pushedUpdate' | 'editorId' | 'registeredToolbarFrames' | 'suppliedFocusPreservingFrames' | 'isFocused' | 'nativeViewRef' | 'refreshFocusPreservingFrames' | 'accessibilityLabel' | 'accessibilityHint' | 'placeholder' | 'autoFocus' | 'autoCapitalize' | 'autoCorrect' | 'keyboardType' | 'allowImageResizing' | 'toolbarFrameOwnerId' | 'onToolbarAction'> & Pick<ReturnType<typeof useRichTextEditorMentions>, 'mentionSuggestionTheme' | 'handleAddonEvent'> & Pick<ReturnType<typeof useRichTextEditorCommands>, 'runAtomAction' | 'updateAtomAttrs' | 'atomOwnerRef' | 'commandToggleMark' | 'commandToggleList' | 'commandToggleHeading' | 'commandToggleBlockquote' | 'commandInsertNode' | 'commandIndentListItem' | 'commandOutdentListItem' | 'openLinkRequest' | 'openImageRequest'> & Pick<ReturnType<typeof useRichTextEditorEvents>, 'handleEditorUpdate' | 'handleEditorError' | 'handleExternalTextCompositionEnd' | 'handleSelectionChange' | 'handleFocusChange' | 'handleContentHeightChange' | 'handleAtomLayout' | 'handleToolbarAction'>): import("react/jsx-runtime").JSX.Element | null;
5
+ export declare function useRichTextEditorPresentation(context: Pick<ReturnType<typeof useRichTextEditorState>, 'theme' | 'addons' | 'imageLoadingPolicy' | 'androidInputOptions' | 'remoteSelections' | 'atoms' | 'atomState' | 'atomComponents' | 'warnedUnknownAtomTypesRef' | 'atomContentWidth' | 'atomPositions' | 'atomViewport' | 'virtualizeAtoms' | 'nativeAtomViewport' | 'selectedKeys' | 'editable' | 'atomsInteractive' | 'documentHandle' | 'activeState' | 'onRequestLink' | 'onRequestImage' | 'toolbarItemsSerializationCacheRef' | 'toolbarItems' | 'document' | 'toolbarPlacement' | 'showToolbar' | 'containerStyle' | 'style' | 'heightBehavior' | 'autoGrowHeight' | 'pushedUpdate' | 'editorId' | 'registeredToolbarFrames' | 'suppliedFocusPreservingFrames' | 'isFocused' | 'nativeViewRef' | 'refreshFocusPreservingFrames' | 'accessibilityLabel' | 'accessibilityHint' | 'placeholder' | 'autoFocus' | 'pasteMode' | 'autoCapitalize' | 'autoCorrect' | 'keyboardType' | 'allowImageResizing' | 'toolbarFrameOwnerId' | 'onToolbarAction'> & Pick<ReturnType<typeof useRichTextEditorMentions>, 'mentionSuggestionTheme' | 'handleAddonEvent'> & Pick<ReturnType<typeof useRichTextEditorCommands>, 'runAtomAction' | 'updateAtomAttrs' | 'atomOwnerRef' | 'commandToggleMark' | 'commandToggleList' | 'commandToggleHeading' | 'commandToggleBlockquote' | 'commandInsertNode' | 'commandIndentListItem' | 'commandOutdentListItem' | 'openLinkRequest' | 'openImageRequest'> & Pick<ReturnType<typeof useRichTextEditorEvents>, 'handleEditorUpdate' | 'handleEditorError' | 'handleExternalTextCompositionEnd' | 'handleSelectionChange' | 'handleFocusChange' | 'handleContentHeightChange' | 'handleAtomLayout' | 'handleToolbarAction'>): import("react/jsx-runtime").JSX.Element | null;
@@ -17,7 +17,7 @@ const EditorToolbar_1 = require("./EditorToolbar");
17
17
  const RichTextEditorSerialization_1 = require("./RichTextEditorSerialization");
18
18
  const RichTextEditorNativeView_1 = require("./RichTextEditorNativeView");
19
19
  function useRichTextEditorPresentation(context) {
20
- const { theme, mentionSuggestionTheme, addons, imageLoadingPolicy, androidInputOptions, remoteSelections, atoms, atomState, atomComponents, warnedUnknownAtomTypesRef, runAtomAction, updateAtomAttrs, atomOwnerRef, atomContentWidth, atomPositions, atomViewport, virtualizeAtoms, nativeAtomViewport, selectedKeys, editable, atomsInteractive, documentHandle, activeState, onRequestLink, onRequestImage, toolbarItemsSerializationCacheRef, toolbarItems, document, toolbarPlacement, showToolbar, containerStyle, style, heightBehavior, autoGrowHeight, pushedUpdate, editorId, registeredToolbarFrames, suppliedFocusPreservingFrames, isFocused, nativeViewRef, refreshFocusPreservingFrames, accessibilityLabel, accessibilityHint, placeholder, autoFocus, autoCapitalize, autoCorrect, keyboardType, allowImageResizing, handleEditorUpdate, handleEditorError, handleExternalTextCompositionEnd, handleSelectionChange, handleFocusChange, handleContentHeightChange, handleAtomLayout, handleToolbarAction, handleAddonEvent, toolbarFrameOwnerId, commandToggleMark, commandToggleList, commandToggleHeading, commandToggleBlockquote, commandInsertNode, commandIndentListItem, commandOutdentListItem, openLinkRequest, openImageRequest, onToolbarAction, } = context;
20
+ const { theme, mentionSuggestionTheme, addons, imageLoadingPolicy, androidInputOptions, remoteSelections, atoms, atomState, atomComponents, warnedUnknownAtomTypesRef, runAtomAction, updateAtomAttrs, atomOwnerRef, atomContentWidth, atomPositions, atomViewport, virtualizeAtoms, nativeAtomViewport, selectedKeys, editable, atomsInteractive, documentHandle, activeState, onRequestLink, onRequestImage, toolbarItemsSerializationCacheRef, toolbarItems, document, toolbarPlacement, showToolbar, containerStyle, style, heightBehavior, autoGrowHeight, pushedUpdate, editorId, registeredToolbarFrames, suppliedFocusPreservingFrames, isFocused, nativeViewRef, refreshFocusPreservingFrames, accessibilityLabel, accessibilityHint, placeholder, autoFocus, pasteMode, autoCapitalize, autoCorrect, keyboardType, allowImageResizing, handleEditorUpdate, handleEditorError, handleExternalTextCompositionEnd, handleSelectionChange, handleFocusChange, handleContentHeightChange, handleAtomLayout, handleToolbarAction, handleAddonEvent, toolbarFrameOwnerId, commandToggleMark, commandToggleList, commandToggleHeading, commandToggleBlockquote, commandInsertNode, commandIndentListItem, commandOutdentListItem, openLinkRequest, openImageRequest, onToolbarAction, } = context;
21
21
  const themeJson = (0, react_1.useMemo)(() => (0, EditorTheme_1.serializeEditorTheme)(theme, mentionSuggestionTheme), [mentionSuggestionTheme, theme]);
22
22
  const addonsJson = (0, RichTextEditorSerialization_1.useSerializedValue)(addons, value => (0, addons_1.serializeNormalizedEditorAddons)(value));
23
23
  const imageLoadingPolicyJson = (0, RichTextEditorSerialization_1.useSerializedValue)(imageLoadingPolicy, value => (0, ImageLoadingPolicy_1.serializeEditorImageLoadingPolicy)(value));
@@ -161,7 +161,7 @@ function useRichTextEditorPresentation(context) {
161
161
  const currentPushedUpdate = pushedUpdate?.editorId === editorId ? pushedUpdate : null;
162
162
  const focusPreservingFrames = [...registeredToolbarFrames, ...suppliedFocusPreservingFrames];
163
163
  const toolbarFrameJson = (0, RichTextEditorSerialization_1.serializeToolbarFrames)(editable && isFocused ? focusPreservingFrames : undefined);
164
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [RichTextEditorNativeView_1.styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(RichTextEditorNativeView_1.NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, onLayout: refreshFocusPreservingFrames, accessibilityLabel: accessibilityLabel, accessibilityHint: accessibilityHint, editorId: editorId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, autoCapitalize: autoCapitalize, autoCorrect: autoCorrect, keyboardType: keyboardType, ...(react_native_1.Platform.OS === 'android' ? { androidInputOptionsJson } : {}), showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, imageLoadingPolicyJson: imageLoadingPolicyJson, themeJson: themeJson, addonsJson: addonsJson, atomsJson: atomsJson, toolbarItemsJson: toolbarItemsJson, toolbarFrameJson: toolbarFrameJson, remoteSelectionsJson: remoteSelectionsJson, editorUpdateJson: currentPushedUpdate?.json, editorUpdateResetJson: currentPushedUpdate?.resetJson, editorUpdateEditorId: currentPushedUpdate?.editorId, editorUpdateRevision: currentPushedUpdate?.revision ?? 0, onEditorUpdate: handleEditorUpdate, onEditorError: handleEditorError, onExternalTextCompositionEnd: handleExternalTextCompositionEnd, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onAtomLayout: handleAtomLayout, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent, children: atomChildren }), shouldRenderJsToolbar ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: 'native-editor-js-toolbar', style: [RichTextEditorNativeView_1.styles.inlineToolbar, { marginTop: inlineToolbarMarginTop }], children: (0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbarFrameOwnerProvider, { ownerId: toolbarFrameOwnerId, children: (0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: document.historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: theme?.toolbar?.showTopBorder ?? false, preserveEditorFocus: false, onToggleMark: commandToggleMark, onToggleListType: (listType) => commandToggleList(listType), onToggleHeading: commandToggleHeading, onToggleBlockquote: commandToggleBlockquote, onInsertNodeType: commandInsertNode, onRunCommand: (command) => {
164
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [RichTextEditorNativeView_1.styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(RichTextEditorNativeView_1.NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, onLayout: refreshFocusPreservingFrames, accessibilityLabel: accessibilityLabel, accessibilityHint: accessibilityHint, editorId: editorId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, pasteMode: pasteMode, autoCapitalize: autoCapitalize, autoCorrect: autoCorrect, keyboardType: keyboardType, ...(react_native_1.Platform.OS === 'android' ? { androidInputOptionsJson } : {}), showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, imageLoadingPolicyJson: imageLoadingPolicyJson, themeJson: themeJson, addonsJson: addonsJson, atomsJson: atomsJson, toolbarItemsJson: toolbarItemsJson, toolbarFrameJson: toolbarFrameJson, remoteSelectionsJson: remoteSelectionsJson, editorUpdateJson: currentPushedUpdate?.json, editorUpdateResetJson: currentPushedUpdate?.resetJson, editorUpdateEditorId: currentPushedUpdate?.editorId, editorUpdateRevision: currentPushedUpdate?.revision ?? 0, onEditorUpdate: handleEditorUpdate, onEditorError: handleEditorError, onExternalTextCompositionEnd: handleExternalTextCompositionEnd, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onAtomLayout: handleAtomLayout, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent, children: atomChildren }), shouldRenderJsToolbar ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: 'native-editor-js-toolbar', style: [RichTextEditorNativeView_1.styles.inlineToolbar, { marginTop: inlineToolbarMarginTop }], children: (0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbarFrameOwnerProvider, { ownerId: toolbarFrameOwnerId, children: (0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: document.historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: theme?.toolbar?.showTopBorder ?? false, preserveEditorFocus: false, onToggleMark: commandToggleMark, onToggleListType: (listType) => commandToggleList(listType), onToggleHeading: commandToggleHeading, onToggleBlockquote: commandToggleBlockquote, onInsertNodeType: commandInsertNode, onRunCommand: (command) => {
165
165
  switch (command) {
166
166
  case 'indentList':
167
167
  commandIndentListItem();
@@ -1,3 +1,4 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
1
2
  import { type EditorToolbarItem } from './EditorToolbar';
2
3
  import type React from 'react';
3
4
  import { type ReadonlyActiveState, type Selection } from './NativeEditorBridge';
@@ -8,7 +9,7 @@ import { type AtomViewport } from './AtomHost';
8
9
  import { type RichTextEditorProps, type RichTextEditorRef } from './RichTextEditorTypes';
9
10
  import { type AtomRenderState } from './RichTextEditorSerialization';
10
11
  import { type NativeEditorViewHandle, type NativeEditorErrorBinding, type NativeAtomPosition } from './RichTextEditorNativeTypes';
11
- export declare function useRichTextEditorState({ documentHandle, documentRevision, value, valueJSON, valueJSONRevision, valueJSONUpdateMode, placeholder, editable, autoFocus, autoCapitalize, autoCorrect, keyboardType, androidInputOptions, heightBehavior, showToolbar, toolbarPlacement, toolbarItems, onToolbarAction, onRequestLink, onRequestImage, imageLoadingPolicy, accessibilityLabel, accessibilityHint, style, containerStyle, theme, addons: addonDescriptors, atoms, atomsInteractive, virtualizeAtoms, atomViewport, remoteSelections, allowImageResizing, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, focusPreservingRefs, onLocalCommit, }: RichTextEditorProps, ref: React.ForwardedRef<RichTextEditorRef>): {
12
+ export declare function useRichTextEditorState({ documentHandle, documentRevision, value, valueJSON, valueJSONRevision, valueJSONUpdateMode, placeholder, editable, pasteMode, autoFocus, autoCapitalize, autoCorrect, keyboardType, androidInputOptions, heightBehavior, showToolbar, toolbarPlacement, toolbarItems, onToolbarAction, onRequestLink, onRequestImage, imageLoadingPolicy, accessibilityLabel, accessibilityHint, style, containerStyle, theme, addons: addonDescriptors, atoms, atomsInteractive, virtualizeAtoms, atomViewport, remoteSelections, allowImageResizing, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, focusPreservingRefs, onLocalCommit, }: RichTextEditorProps, ref: React.ForwardedRef<RichTextEditorRef>): {
12
13
  atomStateRef: React.RefObject<AtomRenderState>;
13
14
  setSelectedKeys: React.Dispatch<React.SetStateAction<ReadonlySet<string>>>;
14
15
  bridge: import("./NativeEditorDocumentBridge").NativeEditorDocumentBridge;
@@ -156,8 +157,8 @@ export declare function useRichTextEditorState({ documentHandle, documentRevisio
156
157
  toolbarItems: readonly EditorToolbarItem[];
157
158
  toolbarPlacement: import("./RichTextEditorTypes").RichTextEditorToolbarPlacement;
158
159
  showToolbar: boolean;
159
- containerStyle: import("react-native").StyleProp<import("react-native").ViewStyle>;
160
- style: import("react-native").StyleProp<import("react-native").ViewStyle>;
160
+ containerStyle: StyleProp<ViewStyle>;
161
+ style: StyleProp<ViewStyle>;
161
162
  autoGrowHeight: number | null;
162
163
  pushedUpdate: {
163
164
  json: string;
@@ -172,6 +173,7 @@ export declare function useRichTextEditorState({ documentHandle, documentRevisio
172
173
  accessibilityHint: string | undefined;
173
174
  placeholder: string | undefined;
174
175
  autoFocus: boolean;
176
+ pasteMode: import("./RichTextEditorTypes").RichTextEditorPasteMode;
175
177
  autoCapitalize: import("./RichTextEditorTypes").RichTextEditorAutoCapitalize | undefined;
176
178
  autoCorrect: boolean | undefined;
177
179
  keyboardType: import("./RichTextEditorTypes").RichTextEditorKeyboardType | undefined;
@@ -10,7 +10,7 @@ const useNativeEditor_1 = require("./useNativeEditor");
10
10
  const addons_1 = require("./addons");
11
11
  const useFocusPreservingFrames_1 = require("./useFocusPreservingFrames");
12
12
  const RichTextEditorSerialization_1 = require("./RichTextEditorSerialization");
13
- function useRichTextEditorState({ documentHandle, documentRevision, value, valueJSON, valueJSONRevision, valueJSONUpdateMode = 'replace', placeholder, editable = true, autoFocus = false, autoCapitalize, autoCorrect, keyboardType, androidInputOptions, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, imageLoadingPolicy, accessibilityLabel, accessibilityHint, style, containerStyle, theme, addons: addonDescriptors, atoms, atomsInteractive = true, virtualizeAtoms = false, atomViewport, remoteSelections, allowImageResizing = true, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, focusPreservingRefs, onLocalCommit, }, ref) {
13
+ function useRichTextEditorState({ documentHandle, documentRevision, value, valueJSON, valueJSONRevision, valueJSONUpdateMode = 'replace', placeholder, editable = true, pasteMode = 'rich', autoFocus = false, autoCapitalize, autoCorrect, keyboardType, androidInputOptions, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, imageLoadingPolicy, accessibilityLabel, accessibilityHint, style, containerStyle, theme, addons: addonDescriptors, atoms, atomsInteractive = true, virtualizeAtoms = false, atomViewport, remoteSelections, allowImageResizing = true, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, focusPreservingRefs, onLocalCommit, }, ref) {
14
14
  const addons = (0, react_1.useMemo)(() => (0, addons_1.normalizeEditorAddons)(addonDescriptors), [addonDescriptors]);
15
15
  (0, NativeEditorBridge_1._assertNativeEditorDocumentHandle)(documentHandle);
16
16
  const documentDescriptor = (0, NativeEditorBridge_1._getNativeEditorDocumentHandleDescriptor)(documentHandle);
@@ -390,8 +390,8 @@ function useRichTextEditorState({ documentHandle, documentRevision, value, value
390
390
  toolbarItems,
391
391
  toolbarPlacement,
392
392
  showToolbar,
393
- containerStyle,
394
- style,
393
+ containerStyle: containerStyle,
394
+ style: style,
395
395
  autoGrowHeight,
396
396
  pushedUpdate,
397
397
  registeredToolbarFrames,
@@ -401,6 +401,7 @@ function useRichTextEditorState({ documentHandle, documentRevision, value, value
401
401
  accessibilityHint,
402
402
  placeholder,
403
403
  autoFocus,
404
+ pasteMode,
404
405
  autoCapitalize,
405
406
  autoCorrect,
406
407
  keyboardType,
@@ -1,7 +1,306 @@
1
1
  import os
2
2
  import UIKit
3
3
 
4
+ enum EditorPasteMode: String {
5
+ case rich
6
+ case plainText
7
+ case disabled
8
+ }
9
+
10
+ struct EditorClipboardPayload {
11
+ static let fragmentType = "com.apollohg.native-editor.fragment"
12
+
13
+ let fragment: String
14
+ let html: String
15
+ let text: String
16
+
17
+ init(fragment: String, html: String, text: String) {
18
+ self.fragment = fragment
19
+ self.html = html
20
+ self.text = text
21
+ }
22
+
23
+ init?(json: String) {
24
+ guard let data = json.data(using: .utf8),
25
+ let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
26
+ object["empty"] as? Bool != true,
27
+ let fragment = object["fragment"] as? String,
28
+ let html = object["html"] as? String,
29
+ let text = object["text"] as? String
30
+ else { return nil }
31
+ self.init(fragment: fragment, html: html, text: text)
32
+ }
33
+
34
+ @discardableResult
35
+ func write(to pasteboard: UIPasteboard) -> Bool {
36
+ pasteboard.items = [[
37
+ Self.fragmentType: Data(fragment.utf8),
38
+ "public.html": Data(html.utf8),
39
+ "public.utf8-plain-text": text
40
+ ]]
41
+ return pasteboard.data(forPasteboardType: Self.fragmentType) != nil
42
+ && pasteboard.data(forPasteboardType: "public.html") != nil
43
+ && pasteboard.string != nil
44
+ }
45
+ }
46
+
47
+ enum EditorClipboardPaste {
48
+ static let maximumRTFBytes = 64 * 1_024 * 1_024
49
+
50
+ static let supportedTypes = [
51
+ EditorClipboardPayload.fragmentType,
52
+ "public.html",
53
+ "public.rtf",
54
+ "public.utf8-plain-text",
55
+ "public.plain-text",
56
+ "public.text"
57
+ ]
58
+
59
+ static func hasSupportedContent(in pasteboard: UIPasteboard) -> Bool {
60
+ pasteboard.contains(pasteboardTypes: supportedTypes)
61
+ }
62
+
63
+ static func command(
64
+ from pasteboard: UIPasteboard,
65
+ mode: EditorPasteMode,
66
+ maximumRTFBytes: Int = EditorClipboardPaste.maximumRTFBytes
67
+ ) -> [String: Any]? {
68
+ guard mode != .disabled else { return nil }
69
+ var command: [String: Any] = ["type": "paste"]
70
+ if let fragment = utf8String(pasteboard.data(forPasteboardType: EditorClipboardPayload.fragmentType)) {
71
+ command["fragment"] = fragment
72
+ }
73
+ var html = utf8String(pasteboard.data(forPasteboardType: "public.html"))
74
+ var text = pasteboard.string
75
+ if html == nil,
76
+ let rtf = pasteboard.data(forPasteboardType: "public.rtf"),
77
+ rtf.count <= maximumRTFBytes,
78
+ let attributed = try? NSAttributedString(
79
+ data: rtf,
80
+ options: [.documentType: NSAttributedString.DocumentType.rtf],
81
+ documentAttributes: nil
82
+ ) {
83
+ let convertedHTML = semanticHTML(from: attributed)
84
+ if !convertedHTML.isEmpty {
85
+ html = convertedHTML
86
+ }
87
+ if text == nil {
88
+ text = attributed.string
89
+ }
90
+ }
91
+ let hasStructuredContent = command["fragment"] != nil || html != nil
92
+ guard hasStructuredContent || !(text?.isEmpty ?? true) else { return nil }
93
+ if let html {
94
+ command["html"] = html
95
+ }
96
+ if let text {
97
+ command["text"] = text
98
+ }
99
+ if mode == .plainText {
100
+ command["plainText"] = true
101
+ }
102
+ return command.count > 1 ? command : nil
103
+ }
104
+
105
+ private static func utf8String(_ data: Data?) -> String? {
106
+ guard let data else { return nil }
107
+ return String(data: data, encoding: .utf8)
108
+ }
109
+
110
+ private static func semanticHTML(from attributed: NSAttributedString) -> String {
111
+ let range = NSRange(location: 0, length: attributed.length)
112
+ var hasList = false
113
+ attributed.enumerateAttribute(.paragraphStyle, in: range) { value, _, stop in
114
+ if let style = value as? NSParagraphStyle, !style.textLists.isEmpty {
115
+ hasList = true
116
+ stop.pointee = true
117
+ }
118
+ }
119
+ guard attributed.string.contains("\n") || attributed.string.contains("\r") || hasList else {
120
+ return semanticInlineHTML(from: attributed, range: range)
121
+ }
122
+
123
+ let string = attributed.string as NSString
124
+ var paragraphs: [(html: String, listTags: [String])] = []
125
+ var location = 0
126
+ while location < string.length {
127
+ let paragraphRange = string.paragraphRange(
128
+ for: NSRange(location: location, length: 0)
129
+ )
130
+ var contentLength = paragraphRange.length
131
+ while contentLength > 0 {
132
+ let scalar = string.character(at: paragraphRange.location + contentLength - 1)
133
+ guard scalar == 0x000A || scalar == 0x000D else { break }
134
+ contentLength -= 1
135
+ }
136
+ let contentRange = NSRange(location: paragraphRange.location, length: contentLength)
137
+ let styleLocation = min(paragraphRange.location, max(attributed.length - 1, 0))
138
+ let style = attributed.length > 0
139
+ ? attributed.attribute(.paragraphStyle, at: styleLocation, effectiveRange: nil)
140
+ as? NSParagraphStyle
141
+ : nil
142
+ paragraphs.append((
143
+ html: semanticInlineHTML(from: attributed, range: contentRange),
144
+ listTags: style?.textLists.map(listTag) ?? []
145
+ ))
146
+ location = NSMaxRange(paragraphRange)
147
+ }
148
+ if let finalScalar = attributed.string.utf16.last,
149
+ finalScalar == 0x000A || finalScalar == 0x000D {
150
+ let style = attributed.attribute(
151
+ .paragraphStyle,
152
+ at: attributed.length - 1,
153
+ effectiveRange: nil
154
+ ) as? NSParagraphStyle
155
+ paragraphs.append((html: "", listTags: style?.textLists.map(listTag) ?? []))
156
+ }
157
+
158
+ var html = ""
159
+ var openListTags: [String] = []
160
+ for paragraph in paragraphs {
161
+ guard !paragraph.listTags.isEmpty else {
162
+ closeLists(&openListTags, into: &html)
163
+ html += "<p>\(paragraph.html)</p>"
164
+ continue
165
+ }
166
+
167
+ let sharedDepth = zip(openListTags, paragraph.listTags)
168
+ .prefix { $0.0 == $0.1 }
169
+ .count
170
+ while openListTags.count > sharedDepth {
171
+ let tag = openListTags.removeLast()
172
+ html += "</li></\(tag)>"
173
+ }
174
+ if !openListTags.isEmpty, openListTags.count == paragraph.listTags.count {
175
+ html += "</li><li>"
176
+ }
177
+ for tag in paragraph.listTags.dropFirst(sharedDepth) {
178
+ html += "<\(tag)><li>"
179
+ openListTags.append(tag)
180
+ }
181
+ html += paragraph.html
182
+ }
183
+ closeLists(&openListTags, into: &html)
184
+ return html
185
+ }
186
+
187
+ private static func semanticInlineHTML(
188
+ from attributed: NSAttributedString,
189
+ range: NSRange
190
+ ) -> String {
191
+ var html = ""
192
+ attributed.enumerateAttributes(in: range) { attributes, range, _ in
193
+ var run = escapeHTMLText(attributed.attributedSubstring(from: range).string)
194
+ if let font = attributes[.font] as? UIFont {
195
+ let traits = font.fontDescriptor.symbolicTraits
196
+ if traits.contains(.traitBold) {
197
+ run = "<strong>\(run)</strong>"
198
+ }
199
+ if traits.contains(.traitItalic) {
200
+ run = "<em>\(run)</em>"
201
+ }
202
+ }
203
+ if styleIsSet(attributes[.underlineStyle]) {
204
+ run = "<u>\(run)</u>"
205
+ }
206
+ if styleIsSet(attributes[.strikethroughStyle]) {
207
+ run = "<s>\(run)</s>"
208
+ }
209
+ if let href = linkString(attributes[.link]) {
210
+ run = "<a href=\"\(escapeHTMLAttribute(href))\">\(run)</a>"
211
+ }
212
+ html += run
213
+ }
214
+ guard !html.isEmpty else { return "" }
215
+ return "<span style=\"white-space:pre-wrap\">\(html)</span>"
216
+ }
217
+
218
+ private static func listTag(_ list: NSTextList) -> String {
219
+ let marker = String(describing: list.markerFormat).lowercased()
220
+ let orderedMarkers = ["decimal", "roman", "alpha", "latin"]
221
+ return orderedMarkers.contains { marker.contains($0) } ? "ol" : "ul"
222
+ }
223
+
224
+ private static func closeLists(_ tags: inout [String], into html: inout String) {
225
+ while let tag = tags.popLast() {
226
+ html += "</li></\(tag)>"
227
+ }
228
+ }
229
+
230
+ private static func escapeHTMLText(_ value: String) -> String {
231
+ value
232
+ .replacingOccurrences(of: "&", with: "&amp;")
233
+ .replacingOccurrences(of: "<", with: "&lt;")
234
+ .replacingOccurrences(of: ">", with: "&gt;")
235
+ .replacingOccurrences(of: "\r\n", with: "\n")
236
+ .replacingOccurrences(of: "\r", with: "\n")
237
+ .replacingOccurrences(of: "\n", with: "<br>")
238
+ }
239
+
240
+ private static func escapeHTMLAttribute(_ value: String) -> String {
241
+ value
242
+ .replacingOccurrences(of: "&", with: "&amp;")
243
+ .replacingOccurrences(of: "<", with: "&lt;")
244
+ .replacingOccurrences(of: ">", with: "&gt;")
245
+ .replacingOccurrences(of: "\"", with: "&quot;")
246
+ .replacingOccurrences(of: "'", with: "&#39;")
247
+ }
248
+
249
+ private static func styleIsSet(_ value: Any?) -> Bool {
250
+ guard let number = value as? NSNumber else { return false }
251
+ return number.intValue != 0
252
+ }
253
+
254
+ private static func linkString(_ value: Any?) -> String? {
255
+ if let url = value as? URL {
256
+ return url.absoluteString
257
+ }
258
+ return value as? String
259
+ }
260
+ }
261
+
4
262
  extension EditorTextView {
263
+ @discardableResult
264
+ func exportSelectionToPasteboard(_ pasteboard: UIPasteboard = .general) -> Bool {
265
+ ensureInternalTextViewDelegate()
266
+ guard editorId != 0 else { return false }
267
+ guard finishExternalTextCompositionBeforeInteractionIfNeeded() else { return false }
268
+ guard prepareForExternalEditorUpdate() else { return false }
269
+ guard syncClipboardSelectionToRust(requiresRange: true) != nil else { return false }
270
+ guard let payload = EditorV2Shadow.clipboardPayload(id: editorId) else { return false }
271
+ return payload.write(to: pasteboard)
272
+ }
273
+
274
+ func syncClipboardSelectionToRust(
275
+ requiresRange: Bool = false
276
+ ) -> (anchor: UInt32, head: UInt32)? {
277
+ guard let selection = currentScalarSelection() else { return nil }
278
+ guard !requiresRange
279
+ || selection.anchor != selection.head
280
+ || selectedImageSelectionState() != nil
281
+ else { return nil }
282
+ let sync: EditorV2SelectionSync?
283
+ if let image = selectedImageSelectionState() {
284
+ sync = EditorV2Shadow.setNodeSelection(id: editorId, docPos: image.docPos)
285
+ } else {
286
+ sync = EditorV2Shadow.setSelectionScalar(
287
+ id: editorId,
288
+ scalarAnchor: selection.anchor,
289
+ scalarHead: selection.head
290
+ )
291
+ }
292
+ guard let sync else { return nil }
293
+ if let refreshed = sync.refreshedUpdateJSON {
294
+ applyUpdateJSON(refreshed, notifyDelegate: false)
295
+ }
296
+ return selection
297
+ }
298
+
299
+ func applyClipboardCommand(_ command: [String: Any]) {
300
+ let updateJSON = EditorV2Shadow.applyClipboardCommand(id: editorId, command: command)
301
+ applyUpdateJSON(updateJSON)
302
+ }
303
+
5
304
  @objc func handleIndentKeyCommand() {
6
305
  handleListDepthKeyCommand(outdent: false)
7
306
  }
@@ -49,7 +49,8 @@ extension EditorTextView {
49
49
  prepareForExternalEditorUpdate(),
50
50
  let position = closestPosition(to: point)
51
51
  else { return false }
52
- let offset = self.offset(from: beginningOfDocument, to: position)
52
+ let caretPosition = autocapitalizationFriendlyEmptyBlockPosition(for: position) ?? position
53
+ let offset = self.offset(from: beginningOfDocument, to: caretPosition)
53
54
  guard !isAtomBoundaryCaretOffset(offset) else { return false }
54
55
  _ = becomeFirstResponder()
55
56
  logicalSelectionScalarRange = nil
@@ -154,7 +155,9 @@ extension EditorTextView {
154
155
  selectedRange.length == 0,
155
156
  let point = caretPlacementTapRecognizer.pendingCaretPoint,
156
157
  let position = closestPosition(to: point) {
157
- let offset = self.offset(from: beginningOfDocument, to: position)
158
+ // Match empty-block normalization so synchronous callbacks cannot alternate offsets.
159
+ let caretPosition = autocapitalizationFriendlyEmptyBlockPosition(for: position) ?? position
160
+ let offset = self.offset(from: beginningOfDocument, to: caretPosition)
158
161
  if selectedRange.location != offset, !isAtomBoundaryCaretOffset(offset) {
159
162
  // Correct UIKit's word snap before publishing or drawing the selection.
160
163
  selectedRange = NSRange(location: offset, length: 0)
@@ -35,6 +35,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
35
35
  var hidesNativeSelectionChrome = false
36
36
  var isPreviewingImageResize = false
37
37
  var allowImageResizing = true
38
+ var pasteMode: EditorPasteMode = .rich
38
39
 
39
40
  override var isEditable: Bool {
40
41
  didSet {
@@ -622,6 +623,11 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
622
623
  input: "\t",
623
624
  modifierFlags: [.shift],
624
625
  action: #selector(handleOutdentKeyCommand)
626
+ ),
627
+ UIKeyCommand(
628
+ input: "v",
629
+ modifierFlags: [.command, .shift],
630
+ action: #selector(pasteAndMatchStyle(_:))
625
631
  )
626
632
  ]
627
633
  }
@@ -875,61 +881,75 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
875
881
  }
876
882
  }
877
883
 
878
- // MARK: - Paste Handling
884
+ // MARK: - Clipboard
885
+
886
+ override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
887
+ guard editorId != 0 else {
888
+ return super.canPerformAction(action, withSender: sender)
889
+ }
890
+ switch action {
891
+ case #selector(copy(_:)):
892
+ return selectedTextRange?.isEmpty == false
893
+ case #selector(cut(_:)):
894
+ return isEditable && selectedTextRange?.isEmpty == false
895
+ case #selector(paste(_:)), #selector(pasteAndMatchStyle(_:)):
896
+ return isEditable
897
+ && pasteMode != .disabled
898
+ && EditorClipboardPaste.hasSupportedContent(in: .general)
899
+ default:
900
+ return super.canPerformAction(action, withSender: sender)
901
+ }
902
+ }
903
+
904
+ override func copy(_ sender: Any?) {
905
+ guard editorId != 0 else {
906
+ super.copy(sender)
907
+ return
908
+ }
909
+ _ = exportSelectionToPasteboard()
910
+ }
911
+
912
+ override func cut(_ sender: Any?) {
913
+ guard editorId != 0 else {
914
+ super.cut(sender)
915
+ return
916
+ }
917
+ guard isEditable, exportSelectionToPasteboard() else { return }
918
+ performInterceptedInput {
919
+ applyClipboardCommand(["type": "paste", "text": "", "plainText": true])
920
+ }
921
+ }
879
922
 
880
- /// Intercept paste operations to route content through Rust.
881
- ///
882
- /// Attempts to extract HTML from the pasteboard first (for rich text paste),
883
- /// falling back to plain text.
884
923
  override func paste(_ sender: Any?) {
924
+ pasteFromPasteboard(forcePlainText: false, sender: sender)
925
+ }
926
+
927
+ override func pasteAndMatchStyle(_ sender: Any?) {
928
+ pasteFromPasteboard(forcePlainText: true, sender: sender)
929
+ }
930
+
931
+ private func pasteFromPasteboard(forcePlainText: Bool, sender: Any?) {
885
932
  ensureInternalTextViewDelegate()
886
933
  guard editorId != 0 else {
887
- super.paste(sender)
934
+ if forcePlainText {
935
+ super.pasteAndMatchStyle(sender)
936
+ } else {
937
+ super.paste(sender)
938
+ }
888
939
  return
889
940
  }
941
+ guard isEditable, pasteMode != .disabled else { return }
890
942
  guard finishExternalTextCompositionBeforeInteractionIfNeeded() else { return }
891
943
  guard prepareForExternalEditorUpdate() else { return }
944
+ guard syncClipboardSelectionToRust() != nil else { return }
945
+ let mode: EditorPasteMode = forcePlainText ? .plainText : pasteMode
946
+ guard let command = EditorClipboardPaste.command(from: .general, mode: mode) else { return }
892
947
 
893
948
  Self.inputLog.debug(
894
949
  "[paste] selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
895
950
  )
896
-
897
- let pasteboard = UIPasteboard.general
898
-
899
- if let htmlData = pasteboard.data(forPasteboardType: "public.html"),
900
- let html = String(data: htmlData, encoding: .utf8) {
901
- performInterceptedInput {
902
- pasteHTML(html)
903
- }
904
- return
905
- }
906
-
907
- if let rtfData = pasteboard.data(forPasteboardType: "public.rtf") {
908
- if let attrStr = try? NSAttributedString(
909
- data: rtfData,
910
- options: [.documentType: NSAttributedString.DocumentType.rtf],
911
- documentAttributes: nil
912
- ) {
913
- if let htmlData = try? attrStr.data(
914
- from: NSRange(location: 0, length: attrStr.length),
915
- documentAttributes: [.documentType: NSAttributedString.DocumentType.html]
916
- ), let html = String(data: htmlData, encoding: .utf8) {
917
- performInterceptedInput {
918
- if !pasteHTML(html, detectContentChange: true),
919
- !attrStr.string.isEmpty {
920
- pastePlainText(attrStr.string)
921
- }
922
- }
923
- return
924
- }
925
- }
926
- }
927
-
928
- if let text = pasteboard.string {
929
- performInterceptedInput {
930
- pastePlainText(text)
931
- }
932
- return
951
+ performInterceptedInput {
952
+ applyClipboardCommand(command)
933
953
  }
934
954
  }
935
955
 
@@ -1,6 +1,28 @@
1
1
  import Foundation
2
2
 
3
3
  extension EditorV2Adapter {
4
+ func clipboardPayloadJSON() -> String? {
5
+ guard beginRuntimeOperation() else { return nil }
6
+ defer { endRuntimeOperation() }
7
+ switch Self.normalizeJsonResult(editorV2GetClipboard(editorId: editorId)) {
8
+ case .failure(let error):
9
+ emit(error)
10
+ return nil
11
+ case .success(let json):
12
+ return json
13
+ }
14
+ }
15
+
16
+ func applyClipboardCommand(_ command: [String: Any]) -> String? {
17
+ guard beginRuntimeOperation() else { return nil }
18
+ defer { endRuntimeOperation() }
19
+ return performMutation(adoptEngineSelection: true) {
20
+ self.callWithEnvelope(["command": command]) { requestJson in
21
+ editorV2ApplyCommand(editorId: self.editorId, requestJson: requestJson)
22
+ }
23
+ }
24
+ }
25
+
4
26
  func toggleMark(_ markType: String, anchor: UInt32, head: UInt32) -> String? {
5
27
  guard beginRuntimeOperation() else { return nil }
6
28
  defer { endRuntimeOperation() }
@@ -71,6 +71,11 @@ enum EditorV2Shadow {
71
71
  adapter(for: id)?.contentSnapshotJSON() ?? "{\"html\":\"\",\"json\":{}}"
72
72
  }
73
73
 
74
+ static func clipboardPayload(id: UInt64) -> EditorClipboardPayload? {
75
+ guard let json = adapter(for: id)?.clipboardPayloadJSON() else { return nil }
76
+ return EditorClipboardPayload(json: json)
77
+ }
78
+
74
79
  // MARK: - State / selection reads
75
80
 
76
81
  static func getCurrentState(id: UInt64) -> String {
@@ -295,6 +300,13 @@ enum EditorV2Shadow {
295
300
  adapter(for: id)?.insertContentHtmlAtEngineSelection(html) ?? "{}"
296
301
  }
297
302
 
303
+ static func applyClipboardCommand(
304
+ id: UInt64,
305
+ command: [String: Any]
306
+ ) -> String {
307
+ adapter(for: id)?.applyClipboardCommand(command) ?? "{}"
308
+ }
309
+
298
310
  static func insertContentJson(id: UInt64, json: String) -> String {
299
311
  adapter(for: id)?.insertContentJsonAtEngineSelection(json) ?? "{}"
300
312
  }
@@ -2077,6 +2077,13 @@ public func editorV2DocToScalar(editorId: String, docPos: UInt32) -> FfiJsonResu
2077
2077
  )
2078
2078
  })
2079
2079
  }
2080
+ public func editorV2GetClipboard(editorId: String) -> FfiJsonResult {
2081
+ return try! FfiConverterTypeFfiJsonResult_lift(try! rustCall() {
2082
+ uniffi_editor_core_fn_func_editor_v2_get_clipboard(
2083
+ FfiConverterString.lower(editorId),$0
2084
+ )
2085
+ })
2086
+ }
2080
2087
  public func editorV2GetContentSnapshot(editorId: String) -> FfiJsonResult {
2081
2088
  return try! FfiConverterTypeFfiJsonResult_lift(try! rustCall() {
2082
2089
  uniffi_editor_core_fn_func_editor_v2_get_content_snapshot(
@@ -2289,6 +2296,9 @@ private let initializationResult: InitializationResult = {
2289
2296
  if (uniffi_editor_core_checksum_func_editor_v2_doc_to_scalar() != 62503) {
2290
2297
  return InitializationResult.apiChecksumMismatch
2291
2298
  }
2299
+ if (uniffi_editor_core_checksum_func_editor_v2_get_clipboard() != 3214) {
2300
+ return InitializationResult.apiChecksumMismatch
2301
+ }
2292
2302
  if (uniffi_editor_core_checksum_func_editor_v2_get_content_snapshot() != 21323) {
2293
2303
  return InitializationResult.apiChecksumMismatch
2294
2304
  }