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 @@ -41,6 +41,13 @@ class NonEditableParagraphFilter : InputFilter {
return ""
}

if (dstart == dend) {
val spanBeforeCursor = getNonEditableSpanBeforeCursor(dest, dstart)
if (spanBeforeCursor != null && dest.getSpanEnd(spanBeforeCursor) == dstart) {
return Strings.NEWLINE_STRING + source
}
}

// Block insert BEFORE non-editable block
if (dstart > 0) {
val before =
Expand Down Expand Up @@ -69,4 +76,18 @@ class NonEditableParagraphFilter : InputFilter {

return null
}

private fun getNonEditableSpanBeforeCursor(
dest: Spanned,
cursorPosition: Int,
): EnrichedNonEditableParagraphSpan? {
if (cursorPosition <= 0) return null

return dest
.getSpans(
cursorPosition - 1,
cursorPosition,
EnrichedNonEditableParagraphSpan::class.java,
).firstOrNull()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.swmansion.enriched.EnrichedTextInputView
import com.swmansion.enriched.events.OnChangeTextEvent
import com.swmansion.enriched.spans.interfaces.EnrichedNonEditableParagraphSpan
import com.swmansion.enriched.utils.InlineSpanPreserver
import com.swmansion.enriched.utils.ParagraphSpanNormalizer
import com.swmansion.enriched.utils.ZWSNormalizer
Expand All @@ -17,6 +18,7 @@ class EnrichedTextWatcher(
private var previousTextLength: Int = 0
private var startCursorPosition: Int = 0
private var prevText: String? = view.text?.toString() ?: ""
private var nonEditableParagraphToRemove: EnrichedNonEditableParagraphSpan? = null

private val inlineSpanPreserver = InlineSpanPreserver()

Expand All @@ -28,6 +30,7 @@ class EnrichedTextWatcher(
) {
previousTextLength = s?.length ?: 0
startCursorPosition = start
nonEditableParagraphToRemove = getNonEditableParagraphBeforeDeletedRange(s, start, count, after)

inlineSpanPreserver.beforeTextChanged(
text = s,
Expand Down Expand Up @@ -62,6 +65,7 @@ class EnrichedTextWatcher(
view.transactionManager.runWithIgnoredSpanWatcher {
inlineSpanPreserver.afterTextChanged()
if (!view.isDuringTransaction) {
removePendingNonEditableParagraph(s)
applyStyles(s)
}
}
Expand All @@ -78,6 +82,35 @@ class EnrichedTextWatcher(
ZWSNormalizer.normalizeNonEmptyParagraphs(s)
}

private fun getNonEditableParagraphBeforeDeletedRange(
text: CharSequence?,
start: Int,
count: Int,
after: Int,
): EnrichedNonEditableParagraphSpan? {
if (text !is Editable || count != 1 || after != 0 || start <= 0) return null

return text
.getSpans(
start - 1,
start,
EnrichedNonEditableParagraphSpan::class.java,
).firstOrNull {
text.getSpanStart(it) < start && text.getSpanEnd(it) >= start
}
}

private fun removePendingNonEditableParagraph(text: Editable) {
val span = nonEditableParagraphToRemove ?: return
nonEditableParagraphToRemove = null

val start = text.getSpanStart(span).coerceIn(0, text.length)
val end = text.getSpanEnd(span).coerceIn(start, text.length)
if (start < end) {
text.delete(start, end)
}
}
Comment thread
IvanIhnatsiuk marked this conversation as resolved.

private fun emitChangeText(text: String?) {
if (!view.shouldEmitOnChangeText) {
return
Expand Down
17 changes: 16 additions & 1 deletion ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#import "Strings.h"
#import "StyleHeaders.h"
#import "TextBlockTapGestureRecognizer.h"
#import "TextInsertionUtils.h"
#import "UIScrollViewKeyboardDismissMode+Parsing.h"
#import "UIView+React.h"
#import "WordsUtils.h"
Expand Down Expand Up @@ -1320,8 +1321,22 @@ - (bool)textView:(UITextView *)textView
if (!isNewLine &&
[ParagraphsUtils isReadOnlyParagraphAtLocation:textView.textStorage
location:range.location]) {
if (text.length == 0)
if (text.length == 0) {
if ([TextInsertionUtils tryDeleteReadOnlyParagraphBeforeRange:range
input:self]) {
[self anyTextMayHaveBeenModified];
return NO;
}
return YES;
}

if ([TextInsertionUtils tryInsertText:text
afterReadOnlyParagraphInRange:range
input:self
paragraphsLimit:_paragraphsLimit]) {
[self anyTextMayHaveBeenModified];
}

return NO;
}
recentlyChangedRange = NSMakeRange(range.location, text.length);
Expand Down
2 changes: 2 additions & 0 deletions ios/utils/Text/ParagraphsUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
+ (NSArray *)getNonNewlineRangesIn:(UITextView *)textView range:(NSRange)range;
+ (BOOL)isReadOnlyParagraphAtLocation:(NSAttributedString *)attributedString
location:(NSUInteger)location;
+ (BOOL)isAtEndOfReadOnlyParagraph:(NSAttributedString *)attributedString
location:(NSUInteger)location;
+ (NSInteger)paragraphsCountInTextView:(UITextView *)textView;
+ (BOOL)isReplacingNewlineInRange:(NSRange)range
text:(NSString *)text
Expand Down
14 changes: 14 additions & 0 deletions ios/utils/Text/ParagraphsUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ + (BOOL)isReadOnlyParagraphAtLocation:(NSAttributedString *)attributedString
return NO;
}

+ (BOOL)isAtEndOfReadOnlyParagraph:(NSAttributedString *)attributedString
location:(NSUInteger)location {
if (location == 0 || location > attributedString.length) {
return NO;
}

NSRange effectiveRange = NSMakeRange(0, 0);
id readOnly = [attributedString attribute:ReadOnlyParagraphKey
atIndex:location - 1
effectiveRange:&effectiveRange];

return readOnly != nil && NSMaxRange(effectiveRange) == location;
}

+ (NSArray<NSValue *> *)separateParagraphRangesInString:(NSString *)string
range:(NSRange)range {
NSRange fullRange = [string paragraphRangeForRange:range];
Expand Down
6 changes: 6 additions & 0 deletions ios/utils/Text/TextInsertionUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
input:(id)input
withSelection:(BOOL)withSelection;
;
+ (BOOL)tryInsertText:(NSString *)text
afterReadOnlyParagraphInRange:(NSRange)range
input:(id)input
paragraphsLimit:(NSInteger)paragraphsLimit;
+ (BOOL)tryDeleteReadOnlyParagraphBeforeRange:(NSRange)range input:(id)input;

+ (void)insertEscapingParagraphsAtIndex:(NSUInteger)index
text:(NSString *)text
attributes:
Expand Down
77 changes: 77 additions & 0 deletions ios/utils/Text/TextInsertionUtils.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#import "TextInsertionUtils.h"
#import "EnrichedTextInputView.h"
#import "ParagraphsUtils.h"
#import "Strings.h"
#import "StyleHeaders.h"
#import "UIView+React.h"

@implementation TextInsertionUtils
Expand Down Expand Up @@ -65,6 +67,81 @@ + (void)replaceText:(NSString *)text
typedInput->recentlyChangedRange = NSMakeRange(range.location, text.length);
}

+ (BOOL)tryInsertText:(NSString *)text
afterReadOnlyParagraphInRange:(NSRange)range
input:(id)input
paragraphsLimit:(NSInteger)paragraphsLimit {
EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input;
if (typedInput == nullptr || text.length == 0 || range.length != 0) {
return NO;
}

UITextView *textView = typedInput->textView;
NSTextStorage *storage = textView.textStorage;
if (![ParagraphsUtils isAtEndOfReadOnlyParagraph:storage
location:range.location]) {
return NO;
}

NSString *replacementText = [NewLine stringByAppendingString:text];
if (paragraphsLimit > 0) {
NSInteger existing = [ParagraphsUtils paragraphsCountInTextView:textView];
NSInteger incoming =
[ParagraphsUtils incomingParagraphsCountFromString:replacementText];

if (existing + incoming - 1 > paragraphsLimit) {
return NO;
}
}

NSAttributedString *replacement = [[NSAttributedString alloc]
initWithString:replacementText
attributes:typedInput->defaultTypingAttributes];
NSRange replacementRange = NSMakeRange(range.location, replacement.length);

[storage beginEditing];
[storage replaceCharactersInRange:range withAttributedString:replacement];
[storage removeAttribute:ReadOnlyParagraphKey range:replacementRange];
[storage endEditing];

textView.selectedRange = NSMakeRange(NSMaxRange(replacementRange), 0);
typedInput->recentlyChangedRange = replacementRange;

return YES;
}

+ (BOOL)tryDeleteReadOnlyParagraphBeforeRange:(NSRange)range input:(id)input {
EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input;
if (typedInput == nullptr || range.length != 1 || range.location == 0) {
return NO;
}

UITextView *textView = typedInput->textView;
NSTextStorage *storage = textView.textStorage;
NSRange readOnlyRange = NSMakeRange(0, 0);
id readOnly = [storage attribute:ReadOnlyParagraphKey
atIndex:range.location - 1
effectiveRange:&readOnlyRange];

if (readOnly == nil || NSMaxRange(readOnlyRange) < range.location) {
return NO;
}
Comment on lines +121 to +128

NSRange deletionRange =
NSMakeRange(readOnlyRange.location,
MAX(NSMaxRange(readOnlyRange), NSMaxRange(range)) -
readOnlyRange.location);

[storage beginEditing];
[storage deleteCharactersInRange:deletionRange];
[storage endEditing];

textView.selectedRange = NSMakeRange(deletionRange.location, 0);
typedInput->recentlyChangedRange = deletionRange;

return YES;
}

+ (void)insertEscapingParagraphsAtIndex:(NSUInteger)index
text:(NSString *)text
attributes:
Expand Down