Enriched Text
@@ -44,6 +49,7 @@ export function TextRenderer({ htmlValue }: TextRendererProps) {
onBlur={handleTextBlur}
onLinkPress={handleLinkPress}
onMentionPress={handleMentionPress}
+ onImagePress={handleImagePress}
>
{htmlValue}
diff --git a/apps/example-web/src/testScreens/TestEnrichedText.tsx b/apps/example-web/src/testScreens/TestEnrichedText.tsx
index 136b2890c..c039e9fd9 100644
--- a/apps/example-web/src/testScreens/TestEnrichedText.tsx
+++ b/apps/example-web/src/testScreens/TestEnrichedText.tsx
@@ -1,6 +1,7 @@
import { useState, type ChangeEvent } from 'react';
import {
EnrichedText,
+ type OnImagePressEvent,
type OnLinkPressEvent,
type OnMentionPressEvent,
} from 'react-native-enriched-html';
@@ -12,6 +13,8 @@ const INITIAL_VALUE = '
';
export function TestEnrichedText() {
const [htmlInput, setHtmlInput] = useState(INITIAL_VALUE);
const [value, setValue] = useState(INITIAL_VALUE);
+ const [lastImagePress, setLastImagePress] =
+ useState
(null);
const [lastLinkPress, setLastLinkPress] = useState(
null
);
@@ -30,6 +33,7 @@ export function TestEnrichedText() {
htmlStyle={WEB_DEFAULT_HTML_STYLE}
onLinkPress={setLastLinkPress}
onMentionPress={setLastMentionPress}
+ onImagePress={setLastImagePress}
>
{value}
@@ -65,6 +69,9 @@ export function TestEnrichedText() {
{value}
+
+ {JSON.stringify(lastImagePress)}
+
{JSON.stringify(lastLinkPress)}
diff --git a/apps/example/src/components/TextRenderer.tsx b/apps/example/src/components/TextRenderer.tsx
index 6fe302c46..9cf55afc7 100644
--- a/apps/example/src/components/TextRenderer.tsx
+++ b/apps/example/src/components/TextRenderer.tsx
@@ -3,6 +3,7 @@ import {
EnrichedText,
type OnLinkPressEvent,
type OnMentionPressEvent,
+ type OnImagePressEvent,
} from 'react-native-enriched-html';
import { enrichedTextHtmlStyle } from '../constants/editorConfig';
@@ -22,6 +23,13 @@ export const TextRenderer = ({ nodes }: TextRendererProps) => {
);
};
+ const handleImagePress = (e: OnImagePressEvent) => {
+ Alert.alert(
+ 'Image Pressed',
+ `You pressed the image: ${JSON.stringify(e.image)}`
+ );
+ };
+
if (nodes.length === 0) {
return null;
}
@@ -35,6 +43,7 @@ export const TextRenderer = ({ nodes }: TextRendererProps) => {
htmlStyle={enrichedTextHtmlStyle}
onLinkPress={handleLinkPress}
onMentionPress={handleMentionPress}
+ onImagePress={handleImagePress}
>
{node}
diff --git a/apps/example/src/screens/EnrichedTextScreen.tsx b/apps/example/src/screens/EnrichedTextScreen.tsx
index de482f1bb..93947ba3d 100644
--- a/apps/example/src/screens/EnrichedTextScreen.tsx
+++ b/apps/example/src/screens/EnrichedTextScreen.tsx
@@ -3,6 +3,7 @@ import { View, StyleSheet, ScrollView, Text } from 'react-native';
import {
EnrichedText,
type EnrichedTextProps,
+ type OnImagePressEvent,
type OnLinkPressEvent,
type OnMentionPressEvent,
} from 'react-native-enriched-html';
@@ -37,6 +38,10 @@ export function EnrichedTextScreen({ onSwitch }: EnrichedTextScreenProps) {
);
};
+ const handleImagePress = (e: OnImagePressEvent) => {
+ setHtml(`You pressed the image: ${JSON.stringify(e.image)}`);
+ };
+
return (
<>
{html}
diff --git a/docs/TEXT_API_REFERENCE.md b/docs/TEXT_API_REFERENCE.md
index af7f375df..7a456cecd 100644
--- a/docs/TEXT_API_REFERENCE.md
+++ b/docs/TEXT_API_REFERENCE.md
@@ -114,6 +114,27 @@ interface OnMentionPressEvent {
| -------------------------------------- | ------------- | ----------------- |
| `(event: OnMentionPressEvent) => void` | - | iOS, Android, Web |
+### `onImagePress`
+
+Called when the user presses an inline image. Receives an `OnImagePressEvent` with the image uri and dimensions.
+
+```ts
+interface OnImagePressEvent {
+ image: {
+ uri: string;
+ width: number;
+ height: number;
+ };
+}
+```
+
+| Type | Default Value | Platform |
+| ------------------------------------ | ------------- | ----------------- |
+| `(event: OnImagePressEvent) => void` | - | iOS, Android, Web |
+
+> [!NOTE]
+> No visual feedback is applied on press.
+
## EnrichedTextHtmlStyle type
Extends [`HtmlStyle`](API_REFERENCE.md#htmlstyle-type) with additional press-state styling for interactive elements. All properties from `HtmlStyle` are supported except `a` and `mention`, which are replaced by the extended versions below.
diff --git a/docs/WEB.md b/docs/WEB.md
index 1c1f9763e..e1226f9df 100644
--- a/docs/WEB.md
+++ b/docs/WEB.md
@@ -41,7 +41,7 @@ See [Web Keyboard Shortcuts](./INPUT_API_REFERENCE.md#web-keyboard-shortcuts) fo
- Customizing the styling using props: `style`, `htmlStyle`, `selectionColor`.
- `selectable` prop
- `useHtmlNormalizer`
-- `onLinkPress` and `onMentionPress` callbacks
+- `onLinkPress`, `onMentionPress` and `onImagePress` callbacks
### Unsupported
diff --git a/ios/EnrichedTextView.h b/ios/EnrichedTextView.h
index e0af096f7..cd5d47243 100644
--- a/ios/EnrichedTextView.h
+++ b/ios/EnrichedTextView.h
@@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
- (CGSize)measureSize:(CGFloat)maxWidth;
- (void)emitOnLinkPressEvent:(NSString *)url;
- (void)emitOnMentionPressEvent:(MentionParams *)mention;
+- (void)emitOnImagePressEvent:(MediaAttachment *)attachment;
@end
NS_ASSUME_NONNULL_END
diff --git a/ios/EnrichedTextView.mm b/ios/EnrichedTextView.mm
index 16ef47083..75e7ded9a 100644
--- a/ios/EnrichedTextView.mm
+++ b/ios/EnrichedTextView.mm
@@ -785,6 +785,21 @@ - (void)emitOnMentionPressEvent:(MentionParams *)mention {
}
}
+- (void)emitOnImagePressEvent:(MediaAttachment *)attachment {
+ if (!attachment)
+ return;
+ auto emitter = [self getEventEmitter];
+ if (emitter != nullptr) {
+ emitter->onImagePress(
+ {.image = {
+ .uri =
+ attachment.uri ? [attachment.uri toCppString] : std::string{},
+ .width = attachment.width,
+ .height = attachment.height,
+ }});
+ }
+}
+
// MARK: - Media attachments delegate
- (void)mediaAttachmentDidUpdate:(MediaAttachment *)attachment {
diff --git a/ios/utils/EnrichedTextTouchHandler.mm b/ios/utils/EnrichedTextTouchHandler.mm
index 255daef6d..48665320c 100644
--- a/ios/utils/EnrichedTextTouchHandler.mm
+++ b/ios/utils/EnrichedTextTouchHandler.mm
@@ -1,6 +1,7 @@
#import "EnrichedTextTouchHandler.h"
#import "ColorExtension.h"
#import "EnrichedTextView.h"
+#import "ImageAttachment.h"
#import "LinkData.h"
#import "MentionParams.h"
#import "MentionStyleProps.h"
@@ -78,8 +79,10 @@ - (id)findClickableAt:(NSUInteger)idx
if (idx == NSNotFound || idx >= self.view.textView.textStorage.length)
return nil;
- NSArray *keys =
- @[ @"EnrichedManualLink", @"EnrichedAutomaticLink", @"EnrichedMention" ];
+ NSArray *keys = @[
+ @"EnrichedManualLink", @"EnrichedAutomaticLink", @"EnrichedMention",
+ @"EnrichedImage"
+ ];
for (NSString *k in keys) {
id val = [self.view.textView.textStorage attribute:k
atIndex:idx
@@ -93,6 +96,10 @@ - (id)findClickableAt:(NSUInteger)idx
}
- (void)updateVisualsPressed:(BOOL)pressed {
+ if ([_activeAttrKey isEqualToString:@"EnrichedImage"]) {
+ return;
+ }
+
if (pressed) {
UIColor *color = nil;
UIColor *bgColor = nil;
@@ -141,6 +148,15 @@ - (void)dispatchEvent {
[self.view emitOnLinkPressEvent:((LinkData *)_activeValue).url];
} else if ([_activeAttrKey isEqualToString:@"EnrichedMention"]) {
[self.view emitOnMentionPressEvent:(MentionParams *)_activeValue];
+ } else if ([_activeAttrKey isEqualToString:@"EnrichedImage"]) {
+ NSRange attachmentRange = _activeRange;
+ ImageAttachment *attachment =
+ [self.view.textView.textStorage attribute:NSAttachmentAttributeName
+ atIndex:attachmentRange.location
+ effectiveRange:NULL];
+ if ([attachment isKindOfClass:[ImageAttachment class]]) {
+ [self.view emitOnImagePressEvent:attachment];
+ }
}
}
diff --git a/src/index.native.tsx b/src/index.native.tsx
index 82d450b16..8f7473c7a 100644
--- a/src/index.native.tsx
+++ b/src/index.native.tsx
@@ -31,4 +31,5 @@ export type {
EnrichedTextHtmlStyle,
OnMentionPressEvent,
OnLinkPressEvent,
+ OnImagePressEvent,
} from './types';
diff --git a/src/index.tsx b/src/index.tsx
index a5b445685..be9bf8c70 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -24,4 +24,5 @@ export type {
OnMentionPressEvent,
OnLinkPressEvent,
TextShortcut,
+ OnImagePressEvent,
} from './types';
diff --git a/src/native/EnrichedText.tsx b/src/native/EnrichedText.tsx
index 7063db285..fbef744a5 100644
--- a/src/native/EnrichedText.tsx
+++ b/src/native/EnrichedText.tsx
@@ -16,6 +16,7 @@ import EnrichedTextNativeComponent, {
type NativeProps,
type OnLinkPressEvent,
type OnMentionPressEventInternal,
+ type OnImagePressEvent,
} from '../spec/EnrichedTextNativeComponent';
import { nullthrows } from '../utils/nullthrows';
import { normalizeEnrichedTextHtmlStyle } from '../utils/normalizeHtmlStyle';
@@ -37,6 +38,7 @@ export const EnrichedText = ({
allowFontScaling = true,
onLinkPress: _onLinkPress,
onMentionPress: _onMentionPress,
+ onImagePress: _onImagePress,
...rest
}: EnrichedTextProps) => {
const nativeRef = useRef(null);
@@ -66,6 +68,13 @@ export const EnrichedText = ({
[_onMentionPress]
);
+ const onImagePress: DirectEventHandler = useCallback(
+ (e) => {
+ _onImagePress?.(e.nativeEvent);
+ },
+ [_onImagePress]
+ );
+
useImperativeHandle(ref, () => ({
measureInWindow: (callback: MeasureInWindowOnSuccessCallback) => {
nullthrows(nativeRef.current).measureInWindow(callback);
@@ -109,6 +118,7 @@ export const EnrichedText = ({
allowFontScaling={allowFontScaling}
onLinkPress={onLinkPress}
onMentionPress={onMentionPress}
+ onImagePress={onImagePress}
{...rest}
/>
);
diff --git a/src/spec/EnrichedTextNativeComponent.ts b/src/spec/EnrichedTextNativeComponent.ts
index fec62bd3d..790165b97 100644
--- a/src/spec/EnrichedTextNativeComponent.ts
+++ b/src/spec/EnrichedTextNativeComponent.ts
@@ -79,6 +79,14 @@ export interface OnMentionPressEvent {
attributes: Record;
}
+export interface OnImagePressEvent {
+ image: {
+ uri: string;
+ width: Float;
+ height: Float;
+ };
+}
+
export interface NativeProps extends ViewProps {
// Custom props
text: string;
@@ -95,6 +103,7 @@ export interface NativeProps extends ViewProps {
// Events
onLinkPress?: DirectEventHandler;
onMentionPress?: DirectEventHandler;
+ onImagePress?: DirectEventHandler;
// Style related props - used for generating proper setters in component's manager
// These should not be passed as regular props
diff --git a/src/types.ts b/src/types.ts
index acda85b41..814225df8 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -836,6 +836,9 @@ export interface EnrichedTextProps extends ViewProps {
/** Called when the user taps a mention node inside the rendered content. */
onMentionPress?: (event: OnMentionPressEvent) => void;
+
+ /** Called when the user taps an inline image inside the rendered content. */
+ onImagePress?: (event: OnImagePressEvent) => void;
}
export interface EnrichedTextMentionStyleProperties extends MentionStyleProperties {
@@ -864,3 +867,11 @@ export interface OnMentionPressEvent {
indicator: string;
attributes: Record;
}
+
+export interface OnImagePressEvent {
+ image: {
+ uri: string;
+ width: number;
+ height: number;
+ };
+}
diff --git a/src/web/EnrichedText.css b/src/web/EnrichedText.css
index 706b8a02e..228177fda 100644
--- a/src/web/EnrichedText.css
+++ b/src/web/EnrichedText.css
@@ -320,6 +320,7 @@
width: var(--et-checkbox-box-size, 24px);
height: var(--et-checkbox-box-size, 24px);
+ pointer-events: none;
accent-color: var(--et-checkbox-box-color, #0000ff);
/* disable checkbox press style as it's read-only */
diff --git a/src/web/EnrichedText.tsx b/src/web/EnrichedText.tsx
index 63141b0d7..fb7ff9c3e 100644
--- a/src/web/EnrichedText.tsx
+++ b/src/web/EnrichedText.tsx
@@ -34,6 +34,7 @@ export const EnrichedText = memo(
onBlur,
onLinkPress,
onMentionPress,
+ onImagePress,
}: EnrichedTextProps) => {
const containerRef = useRef(null);
@@ -97,9 +98,15 @@ export const EnrichedText = memo(
const onLinkPressRef = useStableRef(onLinkPress);
const onMentionPressRef = useStableRef(onMentionPress);
+ const onImagePressRef = useStableRef(onImagePress);
useImageErrorFallback(containerRef);
- usePressInteractions(containerRef, onLinkPressRef, onMentionPressRef);
+ usePressInteractions(
+ containerRef,
+ onLinkPressRef,
+ onMentionPressRef,
+ onImagePressRef
+ );
return (
<>
diff --git a/src/web/usePressInteractions.ts b/src/web/usePressInteractions.ts
index 9768b442e..0856c5c16 100644
--- a/src/web/usePressInteractions.ts
+++ b/src/web/usePressInteractions.ts
@@ -1,5 +1,9 @@
import { useEffect, type RefObject } from 'react';
-import type { OnLinkPressEvent, OnMentionPressEvent } from '../types';
+import type {
+ OnLinkPressEvent,
+ OnMentionPressEvent,
+ OnImagePressEvent,
+} from '../types';
type OnLinkPressEventRef = RefObject<
((event: OnLinkPressEvent) => void) | undefined
@@ -9,10 +13,21 @@ type OnMentionPressEventRef = RefObject<
((event: OnMentionPressEvent) => void) | undefined
>;
+type OnImagePressEventRef = RefObject<
+ ((event: OnImagePressEvent) => void) | undefined
+>;
+
+type ImageAttributes = {
+ uri: string;
+ width: number;
+ height: number;
+};
+
export function usePressInteractions(
containerRef: RefObject,
onLinkPressRef: OnLinkPressEventRef,
- onMentionPressRef: OnMentionPressEventRef
+ onMentionPressRef: OnMentionPressEventRef,
+ onImagePressRef: OnImagePressEventRef
) {
useEffect(() => {
const container = containerRef.current;
@@ -21,6 +36,19 @@ export function usePressInteractions(
const handleInteraction = (e: MouseEvent) => {
const target = e.target as HTMLElement;
+ const image = target.closest('img');
+ if (image && container.contains(image)) {
+ e.preventDefault();
+
+ const imageAttributes = getImageAttributes(image);
+
+ if (imageAttributes) {
+ onImagePressRef.current?.({
+ image: imageAttributes,
+ });
+ }
+ }
+
const checkbox = target.closest("input[type='checkbox']");
if (checkbox && container.contains(checkbox)) {
e.preventDefault();
@@ -56,5 +84,44 @@ export function usePressInteractions(
container.addEventListener('click', handleInteraction);
return () => container.removeEventListener('click', handleInteraction);
- }, [containerRef, onLinkPressRef, onMentionPressRef]);
+ }, [containerRef, onLinkPressRef, onMentionPressRef, onImagePressRef]);
+}
+
+function getImageAttributes(
+ image: HTMLImageElement
+): ImageAttributes | undefined {
+ const uri = image.getAttribute('src');
+
+ if (!uri) {
+ return undefined;
+ }
+
+ const { width, height } = getImageDimensions(image);
+
+ return {
+ uri,
+ width,
+ height,
+ };
+}
+
+function getImageDimensions(image: HTMLImageElement): {
+ width: number;
+ height: number;
+} {
+ const rawWidth = image.getAttribute('width');
+ const rawHeight = image.getAttribute('height');
+
+ if (rawWidth && rawHeight) {
+ const width = parseInt(rawWidth, 10);
+ const height = parseInt(rawHeight, 10);
+
+ if (!isNaN(width) && !isNaN(height)) {
+ return { width, height };
+ }
+ }
+
+ const rect = image.getBoundingClientRect();
+
+ return { width: rect.width, height: rect.height };
}