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 @@ -11,6 +11,7 @@ import android.text.method.LinkMovementMethod
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.view.MotionEvent
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.widget.AppCompatEditText
import com.facebook.react.bridge.Arguments
Expand Down Expand Up @@ -51,6 +52,7 @@ class ReactNativeRichTextEditorView : AppCompatEditText {
private var autoFocus = false
private var typefaceDirty = false
private var didAttachToWindow = false
private var detectScrollMovement = false
private var fontSize: Float? = null
private var fontFamily: String? = null
private var fontStyle: Int = ReactConstants.UNSET
Expand Down Expand Up @@ -82,7 +84,10 @@ class ReactNativeRichTextEditorView : AppCompatEditText {
private fun prepareComponent() {
isSingleLine = false
isHorizontalScrollBarEnabled = false
gravity = android.view.Gravity.CENTER or android.view.Gravity.START
isVerticalScrollBarEnabled = true
gravity = android.view.Gravity.TOP or android.view.Gravity.START
inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE

// required to make ClickableSpans really clickable
movementMethod = LinkMovementMethod.getInstance()

Expand All @@ -93,6 +98,32 @@ class ReactNativeRichTextEditorView : AppCompatEditText {
addTextChangedListener(EditorTextWatcher(this))
}

// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L295C1-L296C1
override fun onTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
detectScrollMovement = true
// Disallow parent views to intercept touch events, until we can detect if we should be
// capturing these touches or not.
this.parent.requestDisallowInterceptTouchEvent(true)
}

MotionEvent.ACTION_MOVE ->
if (detectScrollMovement) {
if (!canScrollVertically(-1) &&
!canScrollVertically(1) &&
!canScrollHorizontally(-1) &&
!canScrollHorizontally(1)) {
// We cannot scroll, let parent views take care of these touches.
this.parent.requestDisallowInterceptTouchEvent(false)
}
detectScrollMovement = false
}
}

return super.onTouchEvent(ev)
}

override fun onSelectionChanged(selStart: Int, selEnd: Int) {
super.onSelectionChanged(selStart, selEnd)
selection?.onSelection(selStart, selEnd)
Expand Down Expand Up @@ -128,6 +159,9 @@ class ReactNativeRichTextEditorView : AppCompatEditText {
// Assign SpanWatcher one more time as our previous spannable has been replaced
addSpanWatcher(EditorSpanWatcher(this))

// Scroll to the last line of text
setSelection(text?.length ?: 0)

isSettingDefaultValue = false
}

Expand Down Expand Up @@ -212,6 +246,16 @@ class ReactNativeRichTextEditorView : AppCompatEditText {
}
}

// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L283C2-L284C1
// After the text changes inside an EditText, TextView checks if a layout() has been requested.
// If it has, it will not scroll the text to the end of the new text inserted, but wait for the
// next layout() to be called. However, we do not perform a layout() after a requestLayout(), so
// we need to override isLayoutRequested to force EditText to scroll to the end of the new text
// immediately.
override fun isLayoutRequested(): Boolean {
return false
}

fun measureSize(maxWidth: Float): Pair<Float, Float> {
val paint = this.paint
val spannable = text as Spannable
Expand Down
24 changes: 20 additions & 4 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Linking,
Alert,
TextInput,
ScrollView,
} from 'react-native';
import {
RichTextInput,
Expand Down Expand Up @@ -53,6 +54,8 @@ const DEFAULT_LINK_STATE = {
url: '',
};

const DEBUG_SCROLLABLE = false;

export default function App() {
const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false);
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
Expand Down Expand Up @@ -153,7 +156,10 @@ export default function App() {

return (
<>
<View style={styles.container}>
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
>
<Text style={styles.label}>SWM Rich Text Editor</Text>
<View style={styles.editor}>
<RichTextInput
Expand Down Expand Up @@ -190,7 +196,8 @@ export default function App() {
/>
<Button title="Focus" onPress={handleFocus} />
<Button title="Blur" onPress={handleBlur} />
</View>
{DEBUG_SCROLLABLE && <View style={styles.scrollPlaceholder} />}
</ScrollView>
<LinkModal
defaults={currentLink}
isOpen={isLinkModalOpen}
Expand All @@ -209,9 +216,11 @@ export default function App() {
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
content: {
flexGrow: 1,
padding: 16,
alignItems: 'center',
},
editor: {
width: '100%',
Expand All @@ -235,7 +244,14 @@ const styles = StyleSheet.create({
defaultInput: {
marginTop: 24,
width: '100%',
height: 40,
borderBottomWidth: 1,
borderBottomColor: 'grey',
},
scrollPlaceholder: {
marginTop: 24,
width: '100%',
height: 1000,
backgroundColor: 'rgb(0, 26, 114)',
},
});