Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.swmansion.enriched

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.UIManagerHelper
import com.swmansion.enriched.utils.EnrichedParser

@ReactModule(name = EnrichedTextInputModule.NAME)
class EnrichedTextInputModule(val reactContext: ReactApplicationContext) :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the fact that this PR introduces a native module. Why exactly is that needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary because native commands do not allow you to return any values. If you have any other ideas on how the second option in this list can be implemented without a module, I would be happy to change the current implementation 🙂

NativeEnrichedTextInputModuleSpec(reactContext) {
override fun getName(): String = NAME

override fun getHTMLValue(inputTag: Double): String? {
UiThreadUtil.assertOnUiThread()
val reactNode = inputTag.toInt()
val enrichedInput = getComponent(reactNode)
return enrichedInput?.getHtmlValue() ?: ""
}

private fun getComponent(reactTag: Int): EnrichedTextInputView? {
return try {
val uiManager = UIManagerHelper.getUIManagerForReactTag(reactContext, reactTag)
uiManager?.resolveView(reactTag) as? EnrichedTextInputView
} catch (_: Throwable) {
null
}
}

companion object {
const val NAME = "EnrichedTextInputModule"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,10 @@ class EnrichedTextInputView : AppCompatEditText {
didAttachToWindow = true
}

fun getHtmlValue(): String? {
return EnrichedParser.toHtml(text)
}

companion object {
const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
package com.swmansion.enriched

import com.facebook.react.ReactPackage
import com.facebook.react.BaseReactPackage
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import java.util.ArrayList
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import java.util.HashMap

class EnrichedTextInputViewPackage : ReactPackage {
class EnrichedTextInputViewPackage : BaseReactPackage() {
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
viewManagers.add(EnrichedTextInputViewManager())
return viewManagers
return listOf(EnrichedTextInputViewManager())
}

override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return emptyList()
}
}
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return if (name == EnrichedTextInputModule.NAME) {
EnrichedTextInputModule(reactContext)
} else {
null
}
}

override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
return ReactModuleInfoProvider {
val moduleMap: MutableMap<String, ReactModuleInfo> = HashMap()
moduleMap[EnrichedTextInputModule.NAME] = ReactModuleInfo(
EnrichedTextInputModule.NAME,
EnrichedTextInputModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
false, // isCxxModule
true // isTurboModule
)
moduleMap
}
}
}
2 changes: 1 addition & 1 deletion android/src/main/new_arch/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/generated/jni)
set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL})

file(GLOB LIB_MODULE_SRCS CONFIGURE_DEPENDS *.cpp react/renderer/components/${LIB_LITERAL}/*.cpp)
file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp)
file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_JNI_DIR}/*.cpp ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp)

add_library(
${LIB_TARGET_NAME}
Expand Down
22 changes: 0 additions & 22 deletions android/src/main/new_arch/RNEnrichedTextInputViewSpec.cpp

This file was deleted.

5 changes: 5 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ export default function App() {
onPress={openValueModal}
style={styles.valueButton}
/>
<Button
title="Get input's HTML value"
onPress={() => console.log(ref.current?.getHTMLValue())}
style={styles.valueButton}
/>
<HtmlSection currentHtml={currentHtml} />
{DEBUG_SCROLLABLE && <View style={styles.scrollPlaceholder} />}
</ScrollView>
Expand Down
20 changes: 20 additions & 0 deletions ios/EnrichedTextInputModule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// EnrichedTextInputModule.h
// ReactNativeEnriched
//
// Created by Ivan Ignathuk on 04/11/2025.
//

#import <Foundation/Foundation.h>
#import <ReactNativeEnriched/RNEnrichedTextInputViewSpec.h>
#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>

NS_ASSUME_NONNULL_BEGIN

@interface EnrichedTextInputModule : NSObject<NativeEnrichedTextInputModuleSpec>
@property (nonatomic, weak) RCTBridge *bridge;

@end

NS_ASSUME_NONNULL_END
46 changes: 46 additions & 0 deletions ios/EnrichedTextInputModule.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// EnrichedTextInputModule.m
// ReactNativeEnriched
//
// Created by Ivan Ignathuk on 04/11/2025.
//

#import "EnrichedTextInputModule.h"
#import "EnrichedTextInputView.h"

static UIView *findViewByReactTag(NSInteger reactTag, RCTBridge *bridge) {
if (bridge.uiManager) {
UIView *view = [bridge.uiManager viewForReactTag:@(reactTag)];
if (view) {
return view;
}
}

return nil;
}

@implementation EnrichedTextInputModule

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeEnrichedTextInputModuleSpecJSI>(params);
}

- (nonnull NSString *)getHTMLValue:(NSInteger)inputTag {
__block NSString *value = @"";

dispatch_sync(dispatch_get_main_queue(), ^{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the fact that we need dispatch_sync here. It blocks the current thread.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I have a compromise approach. We can keep this method and add another one

  1. The new method will work like this: On the main or background thread, we can copy the editable/attributed text value and then parse it on the background thread. Since I think the performant work is done in the parser
  2. We can add a BOOOOLD note that the sync method might block the UI thread, and it's not recommended for usage or use it with caution.

It would be nice to hear your point of view 🙂

UIView *view = findViewByReactTag(inputTag, self.bridge);
if ([view isKindOfClass:[EnrichedTextInputView class]]) {
EnrichedTextInputView *enrichedTextView = (EnrichedTextInputView *)view;
value = [enrichedTextView getHTMLValue];
}
});

return value;
}

+ (NSString *)moduleName {
return @"EnrichedTextInputModule";
}

@end
1 change: 1 addition & 0 deletions ios/EnrichedTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)emitOnMentionEvent:(NSString *)indicator text:(nullable NSString *)text;
- (void)anyTextMayHaveBeenModified;
- (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range;
- (NSString *)getHTMLValue;
@end

NS_ASSUME_NONNULL_END
Expand Down
16 changes: 14 additions & 2 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ - (instancetype)initWithFrame:(CGRect)frame {
[self setupTextView];
[self setupPlaceholderLabel];
self.contentView = textView;
[textView setScrollEnabled: NO];
}
return self;
}
Expand Down Expand Up @@ -157,6 +158,17 @@ - (void)setupPlaceholderLabel {
_placeholderLabel.hidden = YES;
}

// MARK: - HTML Value Getter

- (NSString *)getHTMLValue {
if (textView.textStorage.string.length == 0) {
return @"";
}

NSString *htmlOutput = [parser parseToHtmlFromRange:NSMakeRange(0, textView.textStorage.string.length)];
return htmlOutput ?: @"";
}

// MARK: - Props

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
Expand Down Expand Up @@ -389,7 +401,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
// this way, the newest config attributes are being used!

// the html needs to be generated using the old config
NSString *currentHtml = [parser parseToHtmlFromRange:NSMakeRange(0, textView.textStorage.string.length)];
NSString *currentHtml = [self getHTMLValue];

// now set the new config
config = newConfig;
Expand Down Expand Up @@ -851,7 +863,7 @@ - (void)tryEmittingOnChangeHtmlEvent {
}
auto emitter = [self getEventEmitter];
if(emitter != nullptr) {
NSString *htmlOutput = [parser parseToHtmlFromRange:NSMakeRange(0, textView.textStorage.string.length)];
NSString *htmlOutput = [self getHTMLValue];
// make sure html really changed
if(![htmlOutput isEqualToString:_recentlyEmittedHtml]) {
_recentlyEmittedHtml = htmlOutput;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
},
"codegenConfig": {
"name": "RNEnrichedTextInputViewSpec",
"type": "components",
"type": "all",
"jsSrcsDir": "src",
"outputDir": {
"ios": "ios/generated",
Expand Down
29 changes: 18 additions & 11 deletions src/EnrichedTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@ import EnrichedTextInputNativeComponent, {
type OnMentionDetectedInternal,
type MentionStyleProperties,
} from './EnrichedTextInputNativeComponent';
import type {
ColorValue,
HostInstance,
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
MeasureOnSuccessCallback,
NativeMethods,
NativeSyntheticEvent,
TextStyle,
ViewProps,
ViewStyle,
import {
findNodeHandle,
type ColorValue,
type HostInstance,
type MeasureInWindowOnSuccessCallback,
type MeasureLayoutOnSuccessCallback,
type MeasureOnSuccessCallback,
type NativeMethods,
type NativeSyntheticEvent,
type TextStyle,
type ViewProps,
type ViewStyle,
} from 'react-native';
import { normalizeHtmlStyle } from './normalizeHtmlStyle';
import EnrichedTextInputModule from './NativeEnrichedTextInputModule';

export interface EnrichedTextInputInstance extends NativeMethods {
// General commands
Expand Down Expand Up @@ -59,6 +61,7 @@ export interface EnrichedTextInputInstance extends NativeMethods {
text: string,
attributes?: Record<string, string>
) => void;
getHTMLValue: () => string;
}

export interface OnChangeMentionEvent {
Expand Down Expand Up @@ -295,6 +298,10 @@ export const EnrichedTextInput = ({

Commands.startMention(nullthrows(nativeRef.current), indicator);
},
getHTMLValue: () =>
EnrichedTextInputModule?.getHTMLValue(
nullthrows(findNodeHandle(nativeRef.current))
) ?? '',
}));

const handleMentionEvent = (e: NativeSyntheticEvent<OnMentionEvent>) => {
Expand Down
11 changes: 11 additions & 0 deletions src/NativeEnrichedTextInputModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { TurboModuleRegistry } from 'react-native';
import type { TurboModule } from 'react-native';
import type { Int32 } from 'react-native/Libraries/Types/CodegenTypes';

interface Spec extends TurboModule {
getHTMLValue(inputTag: Int32): string;
}

export default TurboModuleRegistry.getEnforcing<Spec>(
'EnrichedTextInputModule'
);