@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.
- package/README.md +21 -7
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +20 -6
- package/android/src/main/java/com/apollohg/editor/EditorEditTextClipboard.kt +181 -36
- package/android/src/main/java/com/apollohg/editor/EditorEditTextSelection.kt +7 -0
- package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceInteraction.kt +31 -11
- package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +36 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Backend.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +10 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +2 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewProperties.kt +4 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +3 -0
- package/android/src/main/java/com/apollohg/editor/UniffiEditorV2Backend.kt +4 -0
- package/dist/NativeRichTextEditor.d.ts +1 -1
- package/dist/RichTextEditorNativeTypes.d.ts +2 -1
- package/dist/RichTextEditorTypes.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/useRichTextEditorPresentation.d.ts +1 -1
- package/dist/useRichTextEditorPresentation.js +2 -2
- package/dist/useRichTextEditorState.d.ts +5 -3
- package/dist/useRichTextEditorState.js +4 -3
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorTextView+KeyboardInput.swift +299 -0
- package/ios/EditorTextView+Selection.swift +5 -2
- package/ios/EditorTextView.swift +63 -43
- package/ios/EditorV2Adapter+Commands.swift +22 -0
- package/ios/EditorV2Shadow.swift +12 -0
- package/ios/Generated_editor_core.swift +10 -0
- package/ios/NativeEditorExpoView+Properties.swift +4 -0
- package/ios/NativeEditorModule.swift +3 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +11 -0
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +18 -0
- package/src/NativeRichTextEditor.tsx +1 -0
- package/src/RichTextEditorNativeTypes.tsx +2 -0
- package/src/RichTextEditorTypes.tsx +5 -0
- package/src/index.ts +1 -0
- package/src/useRichTextEditorPresentation.tsx +3 -0
- 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:
|
|
160
|
-
style:
|
|
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,
|
|
Binary file
|
|
Binary file
|
|
@@ -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: "&")
|
|
233
|
+
.replacingOccurrences(of: "<", with: "<")
|
|
234
|
+
.replacingOccurrences(of: ">", with: ">")
|
|
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: "&")
|
|
243
|
+
.replacingOccurrences(of: "<", with: "<")
|
|
244
|
+
.replacingOccurrences(of: ">", with: ">")
|
|
245
|
+
.replacingOccurrences(of: "\"", with: """)
|
|
246
|
+
.replacingOccurrences(of: "'", with: "'")
|
|
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
|
|
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
|
-
|
|
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)
|
package/ios/EditorTextView.swift
CHANGED
|
@@ -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: -
|
|
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
|
-
|
|
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
|
-
|
|
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() }
|
package/ios/EditorV2Shadow.swift
CHANGED
|
@@ -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
|
}
|