react-native-arabic-autoheight-text
Version:
Smart Arabic/RTL text renderer for React Native using WebView: auto height, proper justification, line clamp, and skeleton loading.
147 lines (139 loc) • 6.98 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
// src/RtlTextView.tsx
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, Dimensions, } from 'react-native';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
import { WebView } from 'react-native-webview';
const RtlTextView = ({ text, textStyle, numberOfLines, isDark = false, fontFamily, webviewProps, renderSkeleton, skeletonProps, containerStyle, }) => {
const [webViewHeight, setWebViewHeight] = useState(1);
const [loading, setLoading] = useState(true);
const getLastNonNullValue = (styleInput, attribute) => {
if (!Array.isArray(styleInput)) {
return styleInput?.[attribute] ?? null;
}
for (let i = styleInput.length - 1; i >= 0; i--) {
const value = styleInput[i]?.[attribute];
if (value !== null && value !== undefined) {
return value;
}
}
return null;
};
useEffect(() => {
const timer = setTimeout(() => setLoading(false), 1200);
return () => clearTimeout(timer);
}, []);
const color = getLastNonNullValue(textStyle, 'color');
const textAlign = getLastNonNullValue(textStyle, 'textAlign');
const alignSelf = getLastNonNullValue(textStyle, 'alignSelf');
const marginHorizontal = getLastNonNullValue(textStyle, 'marginHorizontal');
const marginStart = getLastNonNullValue(textStyle, 'marginStart');
const marginEnd = getLastNonNullValue(textStyle, 'marginEnd');
const marginTop = getLastNonNullValue(textStyle, 'marginTop');
const marginVertical = getLastNonNullValue(textStyle, 'marginVertical');
const marginBottom = getLastNonNullValue(textStyle, 'marginBottom');
const fontFamilyFromStyle = getLastNonNullValue(textStyle, 'fontFamily');
const fontSize = getLastNonNullValue(textStyle, 'fontSize');
const fontWeight = getLastNonNullValue(textStyle, 'fontWeight');
const lineHeight = getLastNonNullValue(textStyle, 'lineHeight');
const width = getLastNonNullValue(textStyle, 'width');
const backgroundColor = getLastNonNullValue(textStyle, 'backgroundColor');
// 👍 بدون تغيير المنطق: لو فيه fontFamily في الـ props استخدمه، غير كده خُد اللي من الـ style
const effectiveFontFamily = fontFamily ?? fontFamilyFromStyle;
const formattedText = text
? text.replace(/\n/g, '<br>')
: 'No content available';
// 👇 ده هو نفس الـ HTML القديم بالظبط (بس مع effectiveFontFamily)
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@100;200;300;400;500;600;700&display=swap" rel="stylesheet">
<style>
#content {
display: -webkit-box;
-webkit-line-clamp: ${numberOfLines ?? 'unset'};
-webkit-box-orient: vertical;
overflow: hidden;
}
body {
margin-left: ${marginHorizontal === 0 ? marginEnd : marginHorizontal}px;
margin-right: ${marginHorizontal === 0 ? marginStart : marginHorizontal}px;
margin-top: ${!marginVertical ? marginTop : marginVertical}px;
margin-bottom: ${marginVertical === 0 ? marginBottom : marginVertical}px;
font-family: '${effectiveFontFamily}';
font-weight: ${fontWeight ? Number(fontWeight) - 100 : 400};
font-size: ${fontSize}px;
color: ${color ?? '#6C609D'};
direction: rtl;
background-color: ${backgroundColor ?? 'transparent'};
text-align: ${alignSelf === null
? textAlign ?? 'justify'
: alignSelf?.includes('start')
? 'start'
: 'end'};
}
</style>
</head>
<body>
<div id="content">${formattedText}</div>
<script>
(function() {
function updateHeight() {
const body = document.body;
const html = document.documentElement;
const content = document.getElementById('content');
const height = Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight,
content ? content.scrollHeight : 0
);
window.ReactNativeWebView.postMessage(height.toString());
}
window.addEventListener('load', updateHeight);
window.addEventListener('resize', updateHeight);
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(updateHeight);
} else {
setTimeout(updateHeight, 1000);
}
})();
</script>
</body>
</html>
`;
return (_jsxs(View, { style: [
styles.container,
{
height: loading ? 'auto' : webViewHeight,
alignSelf: alignSelf ?? 'auto',
width: width ?? '100%',
},
containerStyle, // ✅ بس بنضيف أي containerStyle من برّه
], children: [_jsx(WebView
// ✅ نسمح للمستخدم يمرر أي props إضافية لكن من غير ما نغيّر منطقنا
, { ...webviewProps, originWhitelist: webviewProps?.originWhitelist ?? ['*'], source: { html: htmlContent }, style: [{ backgroundColor: 'transparent' }, webviewProps?.style], scrollEnabled: false, onMessage: event => {
const newHeight = Number(event.nativeEvent.data);
if (newHeight && newHeight !== webViewHeight) {
setWebViewHeight(newHeight);
}
// لو المستخدم مرّر onMessage خاص بيه، نناديه برضه
if (webviewProps && typeof webviewProps.onMessage === 'function') {
webviewProps.onMessage(event);
}
} }), loading &&
(renderSkeleton ? (renderSkeleton()) : (_jsx(View, { style: { alignItems: 'center', justifyContent: 'center' }, children: _jsx(SkeletonPlaceholder, { backgroundColor: isDark ? '#2A2345' : '#F1FAFF', highlightColor: isDark ? '#3D3660' : '#fff', speed: 1500, ...skeletonProps, children: _jsx(SkeletonPlaceholder.Item, { width: Dimensions.get('window').width - 40, height: '100%', borderRadius: 10 }) }) })))] }));
};
const styles = StyleSheet.create({
container: {
flex: 0,
width: '100%',
},
});
export default React.memo(RtlTextView);