Skip to content

Commit 71959ff

Browse files
christophpurrerappden
authored andcommitted
Add undo/redo support for TextInput (microsoft#1274)
Setting `allowsUndo = YES` and having the `NSTextView` delegate return a stable `NSUndoManager` mostly makes undo/redo work, but we also needed to register an undo action whenever the input contents was changed to something unexpected by JS. This also breaks undo coalescing in a couple key places to make the input behave naturally like other apps. Co-authored-by: Scott Kyle <skyle@fb.com> # Conflicts: # Libraries/Text/TextInput/Multiline/RCTUITextView.m
1 parent 998fe96 commit 71959ff

File tree

3 files changed

+39
-4
lines changed

3 files changed

+39
-4
lines changed

Libraries/Text/TextInput/Multiline/RCTUITextView.m

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ - (instancetype)initWithFrame:(CGRect)frame
5151
self.insertionPointColor = [NSColor selectedControlColor];
5252
// Fix blurry text on non-retina displays.
5353
self.canDrawSubviewsIntoLayer = YES;
54+
self.allowsUndo = YES;
5455
#endif // ]TODO(macOS GH#774)
5556

5657
_textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self];
@@ -185,6 +186,18 @@ - (BOOL)becomeFirstResponder
185186

186187
return success;
187188
}
189+
190+
- (BOOL)resignFirstResponder
191+
{
192+
BOOL success = [super resignFirstResponder];
193+
194+
if (success) {
195+
// Break undo coalescing when losing focus.
196+
[self breakUndoCoalescing];
197+
}
198+
199+
return success;
200+
}
188201
#endif // ]TODO(macOS GH#774)
189202

190203
- (void)setDefaultTextAttributes:(NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
@@ -239,8 +252,16 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
239252
#if !TARGET_OS_OSX // TODO(macOS GH#774)
240253
[super setAttributedText:attributedText];
241254
#else // [TODO(macOS GH#774)
242-
if (attributedText != nil) {
243-
[self.textStorage setAttributedString:attributedText];
255+
if (![self.textStorage isEqualTo:attributedText.string]) {
256+
// Break undo coalescing when the text is changed by JS (e.g. autocomplete).
257+
[self breakUndoCoalescing];
258+
259+
if (attributedText != nil) {
260+
[self.textStorage setAttributedString:attributedText];
261+
} else {
262+
// Avoid Exception thrown while executing UI block: *** -[NSBigMutableString replaceCharactersInRange:withString:]: nil argument
263+
[self.textStorage setAttributedString:[NSAttributedString new]];
264+
}
244265
} else {
245266
// Avoid Exception thrown while executing UI block: *** -[NSBigMutableString replaceCharactersInRange:withString:]: nil argument
246267
[self.textStorage setAttributedString:[NSAttributedString new]];

Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ @implementation RCTBackedTextViewDelegateAdapter {
270270
UITextRange *_previousSelectedTextRange;
271271
#else // [TODO(macOS GH#774)
272272
NSRange _previousSelectedTextRange;
273+
NSUndoManager *_undoManager;
273274
#endif // ]TODO(macOS GH#774)
274275
}
275276

@@ -445,6 +446,13 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
445446
return commandHandled;
446447
}
447448

449+
- (NSUndoManager *)undoManagerForTextView:(NSTextView *)textView {
450+
if (!_undoManager) {
451+
_undoManager = [NSUndoManager new];
452+
}
453+
return _undoManager;
454+
}
455+
448456
#endif // ]TODO(macOS GH#774)
449457

450458
#pragma mark - Public Interface

Libraries/Text/TextInput/RCTBaseTextInputView.m

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
145145
BOOL textNeedsUpdate = NO;
146146
// Remove tag attribute to ensure correct attributed string comparison.
147147
NSMutableAttributedString *const backedTextInputViewTextCopy = [self.backedTextInputView.attributedText mutableCopy];
148-
NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy];
148+
NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy] ?: [NSMutableAttributedString new];
149149

150150
[backedTextInputViewTextCopy removeAttribute:RCTTextAttributesTagAttributeName
151151
range:NSMakeRange(0, backedTextInputViewTextCopy.length)];
@@ -161,7 +161,13 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
161161
#else // [TODO(macOS GH#774)
162162
NSRange selection = [self.backedTextInputView selectedTextRange];
163163
#endif // ]TODO(macOS GH#774)
164-
NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length;
164+
NSAttributedString *oldAttributedText = [self.backedTextInputView.attributedText copy];
165+
NSInteger oldTextLength = oldAttributedText.string.length;
166+
167+
[self.backedTextInputView.undoManager registerUndoWithTarget:self handler:^(RCTBaseTextInputView *strongSelf) {
168+
strongSelf.attributedText = oldAttributedText;
169+
[strongSelf textInputDidChange];
170+
}];
165171

166172
self.backedTextInputView.attributedText = attributedText;
167173

0 commit comments

Comments
 (0)