UNPKG

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
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);