Skip to content
Merged
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
Expand Up @@ -29,6 +29,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.swmansion.enriched.events.MentionHandler
import com.swmansion.enriched.events.OnInputBlurEvent
import com.swmansion.enriched.events.OnInputFocusEvent
import com.swmansion.enriched.events.OnRequestHtmlResultEvent
import com.swmansion.enriched.spans.EnrichedImageSpan
import com.swmansion.enriched.spans.EnrichedSpans
import com.swmansion.enriched.styles.InlineStyles
Expand Down Expand Up @@ -566,6 +567,19 @@ class EnrichedTextInputView : AppCompatEditText {
parametrizedStyles?.setMentionSpan(text, indicator, attributes)
}

fun requestHTML(requestId: Int) {
val html = try {
EnrichedParser.toHtmlWithDefault(text)
} catch (e: Exception) {
null
}

val reactContext = context as ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
dispatcher?.dispatchEvent(OnRequestHtmlResultEvent(surfaceId, id, requestId, html, experimentalSynchronousEvents))
}

// Sometimes setting up style triggers many changes in sequence
// Eg. removing conflicting styles -> changing text -> applying spans
// In such scenario we want to prevent from handling side effects (eg. onTextChanged)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.swmansion.enriched.events.OnInputFocusEvent
import com.swmansion.enriched.events.OnLinkDetectedEvent
import com.swmansion.enriched.events.OnMentionDetectedEvent
import com.swmansion.enriched.events.OnMentionEvent
import com.swmansion.enriched.events.OnRequestHtmlResultEvent
import com.swmansion.enriched.spans.EnrichedSpans
import com.swmansion.enriched.styles.HtmlStyle
import com.swmansion.enriched.utils.jsonStringToStringMap
Expand Down Expand Up @@ -71,6 +72,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
map.put(OnMentionDetectedEvent.EVENT_NAME, mapOf("registrationName" to OnMentionDetectedEvent.EVENT_NAME))
map.put(OnMentionEvent.EVENT_NAME, mapOf("registrationName" to OnMentionEvent.EVENT_NAME))
map.put(OnChangeSelectionEvent.EVENT_NAME, mapOf("registrationName" to OnChangeSelectionEvent.EVENT_NAME))
map.put(OnRequestHtmlResultEvent.EVENT_NAME, mapOf("registrationName" to OnRequestHtmlResultEvent.EVENT_NAME))
Comment thread
exploIF marked this conversation as resolved.

return map
}
Expand Down Expand Up @@ -268,6 +270,10 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
view?.addMention(text, indicator, attributes)
}

override fun requestHTML(view: EnrichedTextInputView?, requestId: Int) {
Comment thread
exploIF marked this conversation as resolved.
view?.requestHTML(requestId)
}

override fun measure(
context: Context,
localData: ReadableMap?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.swmansion.enriched.events

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class OnRequestHtmlResultEvent(
Comment thread
exploIF marked this conversation as resolved.
surfaceId: Int,
viewId: Int,
private val requestId: Int,
private val html: String?,
private val experimentalSynchronousEvents: Boolean
) : Event<OnRequestHtmlResultEvent>(surfaceId, viewId) {

override fun getEventName(): String = EVENT_NAME

override fun getEventData(): WritableMap {
val eventData: WritableMap = Arguments.createMap()
eventData.putInt("requestId", requestId)
if (html != null) {
eventData.putString("html", html)
} else {
eventData.putNull("html")
}
return eventData
}

override fun experimental_isSynchronous(): Boolean = experimentalSynchronousEvents

companion object {
const val EVENT_NAME: String = "onRequestHtmlResult"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ public static String toHtml(Spanned text) {
String normalizedBlockQuote = normalizedCodeBlock.replaceAll("</blockquote>\\n<br>", "</blockquote>");
return "<html>\n" + normalizedBlockQuote + "</html>";
}

public static String toHtmlWithDefault(CharSequence text) {
if (text instanceof Spanned) {
return toHtml((Spanned) text);
}
return "<html>\n<p></p>\n</html>";
}

/**
* Returns an HTML escaped representation of the given plain text.
*/
Expand Down
8 changes: 8 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@ focus: () => void;

Focuses the input.

### `.getHTML()`

```ts
getHTML: () => Promise<string>;
```

Returns a Promise that resolves with the current HTML content of the input. This is useful when you need to get the HTML on-demand (e.g., when saving) without the performance overhead of continuous HTML parsing via `onChangeHtml`.

### `.setImage()`

```ts
Expand Down
19 changes: 19 additions & 0 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
CGFloat imgHeight = [(NSNumber *)args[2] floatValue];

[self addImage:uri width:imgWidth height:imgHeight];
} else if ([commandName isEqualToString:@"requestHTML"]) {
NSInteger requestId = [((NSNumber *)args[0]) integerValue];
[self requestHTML:requestId];
}
}

Expand Down Expand Up @@ -1072,6 +1075,22 @@ - (void)tryEmittingOnChangeHtmlEvent {
}
}

- (void)requestHTML:(NSInteger)requestId {
auto emitter = [self getEventEmitter];
if (emitter != nullptr) {
@try {
NSString *htmlOutput = [parser
parseToHtmlFromRange:NSMakeRange(0,
textView.textStorage.string.length)];
emitter->onRequestHtmlResult({.requestId = static_cast<int>(requestId),
.html = [htmlOutput toCppString]});
} @catch (NSException *exception) {
emitter->onRequestHtmlResult({.requestId = static_cast<int>(requestId),
.html = folly::dynamic(nullptr)});
}
}
}

// MARK: - Styles manipulation

- (void)toggleRegularStyle:(StyleType)type {
Expand Down
45 changes: 45 additions & 0 deletions src/EnrichedTextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type Component,
type RefObject,
useEffect,
useImperativeHandle,
useMemo,
useRef,
Expand All @@ -16,6 +17,7 @@ import EnrichedTextInputNativeComponent, {
type OnMentionEvent,
type OnMentionDetected,
type OnMentionDetectedInternal,
type OnRequestHtmlResultEvent,
type MentionStyleProperties,
} from './EnrichedTextInputNativeComponent';
import type {
Expand All @@ -37,6 +39,7 @@ export interface EnrichedTextInputInstance extends NativeMethods {
focus: () => void;
blur: () => void;
setValue: (value: string) => void;
getHTML: () => Promise<string>;

// Text formatting commands
toggleBold: () => void;
Expand Down Expand Up @@ -164,6 +167,11 @@ const warnAboutMissconfiguredMentions = (indicator: string) => {

type ComponentType = (Component<NativeProps, {}, any> & NativeMethods) | null;

type HtmlRequest = {
resolve: (html: string) => void;
reject: (error: Error) => void;
};

export const EnrichedTextInput = ({
ref,
autoFocus,
Expand Down Expand Up @@ -194,6 +202,19 @@ export const EnrichedTextInput = ({
}: EnrichedTextInputProps) => {
const nativeRef = useRef<ComponentType | null>(null);

const nextHtmlRequestId = useRef(1);
const pendingHtmlRequests = useRef(new Map<number, HtmlRequest>());

useEffect(() => {
const pendingRequests = pendingHtmlRequests.current;
return () => {
pendingRequests.forEach(({ reject }) => {
reject(new Error('Component unmounted'));
});
pendingRequests.clear();
};
}, []);

const normalizedHtmlStyle = useMemo(
() => normalizeHtmlStyle(htmlStyle, mentionIndicators),
[htmlStyle, mentionIndicators]
Expand Down Expand Up @@ -229,6 +250,13 @@ export const EnrichedTextInput = ({
setValue: (value: string) => {
Commands.setValue(nullthrows(nativeRef.current), value);
},
getHTML: () => {
return new Promise<string>((resolve, reject) => {
const requestId = nextHtmlRequestId.current++;
pendingHtmlRequests.current.set(requestId, { resolve, reject });
Commands.requestHTML(nullthrows(nativeRef.current), requestId);
});
},
toggleBold: () => {
Commands.toggleBold(nullthrows(nativeRef.current));
},
Expand Down Expand Up @@ -323,6 +351,22 @@ export const EnrichedTextInput = ({
onMentionDetected?.({ text, indicator, attributes });
};

const handleRequestHtmlResult = (
e: NativeSyntheticEvent<OnRequestHtmlResultEvent>
) => {
const { requestId, html } = e.nativeEvent;
const pending = pendingHtmlRequests.current.get(requestId);
if (!pending) return;

if (html === null || typeof html !== 'string') {
pending.reject(new Error('Failed to parse HTML'));
} else {
pending.resolve(html);
}

pendingHtmlRequests.current.delete(requestId);
};

return (
<EnrichedTextInputNativeComponent
ref={nativeRef}
Expand All @@ -347,6 +391,7 @@ export const EnrichedTextInput = ({
onMentionDetected={handleMentionDetected}
onMention={handleMentionEvent}
onChangeSelection={onChangeSelection}
onRequestHtmlResult={handleRequestHtmlResult}
androidExperimentalSynchronousEvents={
androidExperimentalSynchronousEvents
}
Expand Down
11 changes: 11 additions & 0 deletions src/EnrichedTextInputNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export interface OnChangeSelectionEvent {
text: string;
}

export interface OnRequestHtmlResultEvent {
requestId: Int32;
html: UnsafeMixed;
Comment thread
exploIF marked this conversation as resolved.
}

export interface MentionStyleProperties {
color?: ColorValue;
backgroundColor?: ColorValue;
Expand Down Expand Up @@ -143,6 +148,7 @@ export interface NativeProps extends ViewProps {
onMentionDetected?: DirectEventHandler<OnMentionDetectedInternal>;
onMention?: DirectEventHandler<OnMentionEvent>;
onChangeSelection?: DirectEventHandler<OnChangeSelectionEvent>;
onRequestHtmlResult?: DirectEventHandler<OnRequestHtmlResultEvent>;

// Style related props - used for generating proper setters in component's manager
// These should not be passed as regular props
Expand Down Expand Up @@ -203,6 +209,10 @@ interface NativeCommands {
text: string,
payload: string
) => void;
requestHTML: (
viewRef: React.ElementRef<ComponentType>,
requestId: Int32
) => void;
}

export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
Expand All @@ -229,6 +239,7 @@ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
'addImage',
'startMention',
'addMention',
'requestHTML',
],
});

Expand Down
Loading