not-react-native-macos 0.87.1-rc.7 → 0.87.1-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -142,9 +142,19 @@ typedef NS_ENUM(NSInteger, UIKeyboardAppearance) {
|
|
|
142
142
|
- (void)textViewDidBeginEditing:(id)textView;
|
|
143
143
|
- (void)textViewDidChange:(id)textView;
|
|
144
144
|
- (void)textViewDidEndEditing:(id)textView;
|
|
145
|
+
- (void)textViewDidChangeSelection:(id)textView;
|
|
145
146
|
@end
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
// The runtime name is deliberately not `UITextField`.
|
|
149
|
+
//
|
|
150
|
+
// The compile-time name is what this shim is for, and @compatibility_alias
|
|
151
|
+
// gives that without registering the name with the ObjC runtime. That matters:
|
|
152
|
+
// several Apple frameworks decide whether a process is Catalyst by asking
|
|
153
|
+
// NSClassFromString for a UIKit class. macOS's one-time-code AutoFill does it
|
|
154
|
+
// for the field that holds focus, and answering yes sends it into
|
|
155
|
+
// UIKitMacHelper, which dlopens a UIKit.framework that does not exist on this
|
|
156
|
+
// platform and takes the process down with it.
|
|
157
|
+
@interface RCTUIKitCompatTextField : NSTextField <UITextInput, UITextInputTraits>
|
|
148
158
|
@property (nonatomic, assign) UIKeyboardType keyboardType;
|
|
149
159
|
@property (nonatomic, assign) UIReturnKeyType returnKeyType;
|
|
150
160
|
@property (nonatomic, assign) UITextAutocapitalizationType autocapitalizationType;
|
|
@@ -213,6 +223,7 @@ typedef NS_ENUM(NSInteger, UIKeyboardAppearance) {
|
|
|
213
223
|
- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(UIControlEvents)controlEvents;
|
|
214
224
|
@property (nonatomic, weak, nullable) id<UITextDropDelegate> textDropDelegate;
|
|
215
225
|
@end
|
|
226
|
+
@compatibility_alias UITextField RCTUIKitCompatTextField;
|
|
216
227
|
|
|
217
228
|
// UIKit's per-rect selection geometry. NSTextView exposes selection as ranges,
|
|
218
229
|
// so this is a value object the text layer fills in.
|
|
@@ -78,8 +78,9 @@
|
|
|
78
78
|
|
|
79
79
|
@end
|
|
80
80
|
|
|
81
|
-
@implementation
|
|
81
|
+
@implementation RCTUIKitCompatTextField {
|
|
82
82
|
NSDictionary<NSAttributedStringKey, id> *_typingAttributes;
|
|
83
|
+
NSMutableArray<NSArray *> *_controlEventTargets;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
|
|
@@ -143,6 +144,73 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/**
|
|
148
|
+
* NSTextField arrives configured as a form control: bezelled, opaque, and with
|
|
149
|
+
* its own focus ring. React Native draws all of that itself on the view that
|
|
150
|
+
* owns this one, so the field has to be stripped back to just the text.
|
|
151
|
+
*
|
|
152
|
+
* `selectable` is the one that matters for behaviour rather than looks. AppKit
|
|
153
|
+
* will not begin editing a field it cannot select -- `acceptsFirstResponder`
|
|
154
|
+
* returns NO, the click does nothing, and no field editor is ever installed.
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A click on text input must never drag the window.
|
|
159
|
+
*
|
|
160
|
+
* React Native's view defaults `mouseDownCanMoveWindow` to YES, matching
|
|
161
|
+
* AppKit, and AppKit asks the view under the cursor before delivering the
|
|
162
|
+
* event at all -- so a text view that inherits YES swallows its own clicks and
|
|
163
|
+
* starts a zero-pixel window drag instead. There is no mouseDown to debug,
|
|
164
|
+
* which is what makes it worth a comment.
|
|
165
|
+
*/
|
|
166
|
+
- (BOOL)mouseDownCanMoveWindow
|
|
167
|
+
{
|
|
168
|
+
return NO;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
- (void)uikitCompat_configureForTextInput
|
|
172
|
+
{
|
|
173
|
+
[super setEditable:YES];
|
|
174
|
+
self.selectable = YES;
|
|
175
|
+
self.bezeled = NO;
|
|
176
|
+
self.bordered = NO;
|
|
177
|
+
self.drawsBackground = NO;
|
|
178
|
+
self.focusRingType = NSFocusRingTypeNone;
|
|
179
|
+
self.usesSingleLineMode = YES;
|
|
180
|
+
self.cell.scrollable = YES;
|
|
181
|
+
self.cell.wraps = NO;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
- (instancetype)initWithFrame:(NSRect)frame
|
|
185
|
+
{
|
|
186
|
+
if (self = [super initWithFrame:frame]) {
|
|
187
|
+
[self uikitCompat_configureForTextInput];
|
|
188
|
+
}
|
|
189
|
+
return self;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
- (instancetype)initWithCoder:(NSCoder *)coder
|
|
193
|
+
{
|
|
194
|
+
if (self = [super initWithCoder:coder]) {
|
|
195
|
+
[self uikitCompat_configureForTextInput];
|
|
196
|
+
}
|
|
197
|
+
return self;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* React Native has no `editable` on UITextField, so RCTUITextField maps the
|
|
202
|
+
* prop onto `enabled` -- and overrides `isEditable` to answer from it. That
|
|
203
|
+
* leaves AppKit's own editable and selectable flags untouched, which is what
|
|
204
|
+
* actually decides whether a click starts editing. Keep them in step here,
|
|
205
|
+
* where the mapping is visible, rather than asking upstream to know about it.
|
|
206
|
+
*/
|
|
207
|
+
- (void)setEnabled:(BOOL)enabled
|
|
208
|
+
{
|
|
209
|
+
[super setEnabled:enabled];
|
|
210
|
+
[super setEditable:enabled];
|
|
211
|
+
self.selectable = enabled;
|
|
212
|
+
}
|
|
213
|
+
|
|
146
214
|
- (NSAttributedString *)attributedText
|
|
147
215
|
{
|
|
148
216
|
return self.attributedStringValue;
|
|
@@ -280,16 +348,131 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
|
|
|
280
348
|
return [self respondsToSelector:action];
|
|
281
349
|
}
|
|
282
350
|
|
|
283
|
-
|
|
351
|
+
/**
|
|
352
|
+
* UIKit registers a target/action pair per control event; NSControl carries
|
|
353
|
+
* exactly one pair, fired when editing *ends*. Collapsing the two loses the
|
|
354
|
+
* distinction that matters most here -- RCTBackedTextFieldDelegateAdapter
|
|
355
|
+
* registers for EditingChanged and EditingDidEndOnExit, and with one slot the
|
|
356
|
+
* second registration silently replaces the first. The result is an input that
|
|
357
|
+
* accepts typing and never reports it, which is how `onChangeText` came to
|
|
358
|
+
* never fire.
|
|
359
|
+
*
|
|
360
|
+
* So the pairs are kept per event here, and driven from the AppKit
|
|
361
|
+
* notifications that actually correspond to them.
|
|
362
|
+
*/
|
|
363
|
+
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
|
|
364
|
+
{
|
|
365
|
+
if (target == nil || action == NULL) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (_controlEventTargets == nil) {
|
|
369
|
+
_controlEventTargets = [NSMutableArray new];
|
|
370
|
+
}
|
|
371
|
+
[_controlEventTargets addObject:@[
|
|
372
|
+
[NSValue valueWithNonretainedObject:target],
|
|
373
|
+
[NSValue valueWithPointer:action],
|
|
374
|
+
@(controlEvents),
|
|
375
|
+
]];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)events
|
|
379
|
+
{
|
|
380
|
+
NSMutableArray *kept = [NSMutableArray new];
|
|
381
|
+
for (NSArray *entry in _controlEventTargets) {
|
|
382
|
+
id entryTarget = [entry[0] nonretainedObjectValue];
|
|
383
|
+
SEL entryAction = (SEL)[entry[1] pointerValue];
|
|
384
|
+
UIControlEvents entryEvents = (UIControlEvents)[entry[2] unsignedIntegerValue];
|
|
385
|
+
BOOL matches = (target == nil || entryTarget == target) && (action == NULL || entryAction == action) &&
|
|
386
|
+
(entryEvents & events) != 0;
|
|
387
|
+
if (!matches) {
|
|
388
|
+
[kept addObject:entry];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
_controlEventTargets = kept;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
- (void)uikitCompat_sendActionsForControlEvents:(UIControlEvents)controlEvents
|
|
395
|
+
{
|
|
396
|
+
// Copied first: an action is free to add or remove targets while running.
|
|
397
|
+
for (NSArray *entry in [_controlEventTargets copy]) {
|
|
398
|
+
if (((UIControlEvents)[entry[2] unsignedIntegerValue] & controlEvents) == 0) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
id target = [entry[0] nonretainedObjectValue];
|
|
402
|
+
SEL action = (SEL)[entry[1] pointerValue];
|
|
403
|
+
if ([target respondsToSelector:action]) {
|
|
404
|
+
#pragma clang diagnostic push
|
|
405
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
406
|
+
[target performSelector:action withObject:self];
|
|
407
|
+
#pragma clang diagnostic pop
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The delegate, as UIKit's protocol rather than AppKit's.
|
|
414
|
+
*
|
|
415
|
+
* NSTextField calls `controlTextDidBeginEditing:` and friends; the adapter on
|
|
416
|
+
* the React Native side implements `textFieldDidBeginEditing:` and friends.
|
|
417
|
+
* Same events, different selectors, so nothing was ever called -- which is why
|
|
418
|
+
* `onFocus`, `onBlur` and `onSubmitEditing` stayed silent while typing itself
|
|
419
|
+
* worked.
|
|
420
|
+
*/
|
|
421
|
+
- (id<UITextFieldDelegate>)uikitCompat_uiDelegate
|
|
422
|
+
{
|
|
423
|
+
id delegate = self.delegate;
|
|
424
|
+
return [delegate conformsToProtocol:@protocol(UITextFieldDelegate)] ? delegate : nil;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// NSControl's own hooks, called by the field editor. Preferred over the
|
|
428
|
+
// matching notifications: the notifications are posted onward to the control's
|
|
429
|
+
// delegate, and observing them from the control itself turned out not to see
|
|
430
|
+
// begin and end at all.
|
|
431
|
+
- (void)textDidBeginEditing:(NSNotification *)notification
|
|
284
432
|
{
|
|
285
|
-
|
|
286
|
-
self
|
|
433
|
+
[super textDidBeginEditing:notification];
|
|
434
|
+
[self uikitCompat_sendActionsForControlEvents:UIControlEventEditingDidBegin];
|
|
435
|
+
|
|
436
|
+
id<UITextFieldDelegate> delegate = [self uikitCompat_uiDelegate];
|
|
437
|
+
if ([delegate respondsToSelector:@selector(textFieldDidBeginEditing:)]) {
|
|
438
|
+
[delegate textFieldDidBeginEditing:self];
|
|
439
|
+
}
|
|
287
440
|
}
|
|
288
441
|
|
|
289
|
-
- (void)
|
|
442
|
+
- (void)textDidChange:(NSNotification *)notification
|
|
290
443
|
{
|
|
291
|
-
|
|
292
|
-
self
|
|
444
|
+
[super textDidChange:notification];
|
|
445
|
+
[self uikitCompat_sendActionsForControlEvents:UIControlEventEditingChanged];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* AppKit reports *why* editing ended in the notification's text movement.
|
|
450
|
+
* Return is what UIKit calls EditingDidEndOnExit -- the submit -- while
|
|
451
|
+
* clicking away or tabbing out is a plain EditingDidEnd.
|
|
452
|
+
*/
|
|
453
|
+
- (void)textDidEndEditing:(NSNotification *)notification
|
|
454
|
+
{
|
|
455
|
+
[super textDidEndEditing:notification];
|
|
456
|
+
|
|
457
|
+
NSNumber *movement = notification.userInfo[@"NSTextMovement"];
|
|
458
|
+
BOOL submitted = movement.integerValue == NSReturnTextMovement;
|
|
459
|
+
id<UITextFieldDelegate> delegate = [self uikitCompat_uiDelegate];
|
|
460
|
+
|
|
461
|
+
// Return is a submit before it is an end-of-editing, and the order matters:
|
|
462
|
+
// onSubmitEditing should carry the text, which onBlur may go on to clear.
|
|
463
|
+
if (submitted && [delegate respondsToSelector:@selector(textFieldShouldReturn:)]) {
|
|
464
|
+
[delegate textFieldShouldReturn:self];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
UIControlEvents events = UIControlEventEditingDidEnd;
|
|
468
|
+
if (submitted) {
|
|
469
|
+
events |= UIControlEventEditingDidEndOnExit;
|
|
470
|
+
}
|
|
471
|
+
[self uikitCompat_sendActionsForControlEvents:events];
|
|
472
|
+
|
|
473
|
+
if ([delegate respondsToSelector:@selector(textFieldDidEndEditing:)]) {
|
|
474
|
+
[delegate textFieldDidEndEditing:self];
|
|
475
|
+
}
|
|
293
476
|
}
|
|
294
477
|
|
|
295
478
|
- (id<UITextDropDelegate>)textDropDelegate
|
|
@@ -354,6 +537,123 @@ UIKIT_COMPAT_TEXT_INPUT_GEOMETRY
|
|
|
354
537
|
|
|
355
538
|
@implementation UITextView
|
|
356
539
|
|
|
540
|
+
/**
|
|
541
|
+
* The delegate, as UIKit's protocol rather than AppKit's -- the same mismatch
|
|
542
|
+
* UITextField has. NSTextView tells its delegate through `textDidChange:` and
|
|
543
|
+
* `textViewDidChangeSelection:`; the React Native adapter listens for
|
|
544
|
+
* `textViewDidChange:` and the rest of the UIKit set. Without the bridge a
|
|
545
|
+
* multiline input accepts typing and reports none of it.
|
|
546
|
+
*/
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* A click on text input must never drag the window.
|
|
550
|
+
*
|
|
551
|
+
* React Native's view defaults `mouseDownCanMoveWindow` to YES, matching
|
|
552
|
+
* AppKit, and AppKit asks the view under the cursor before delivering the
|
|
553
|
+
* event at all -- so a text view that inherits YES swallows its own clicks and
|
|
554
|
+
* starts a zero-pixel window drag instead. There is no mouseDown to debug,
|
|
555
|
+
* which is what makes it worth a comment.
|
|
556
|
+
*/
|
|
557
|
+
- (BOOL)mouseDownCanMoveWindow
|
|
558
|
+
{
|
|
559
|
+
return NO;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* NSTextView sizes itself to its text; UITextView fills the frame it is given.
|
|
564
|
+
* Left alone, a multiline input collapses to a single line's height -- it looks
|
|
565
|
+
* right, because React Native's own view draws the background behind it, but
|
|
566
|
+
* only that top strip is hit-testable, so clicking anywhere below the first
|
|
567
|
+
* line does nothing at all.
|
|
568
|
+
*
|
|
569
|
+
* Fixed height with a width-tracking container is what matches UITextView:
|
|
570
|
+
* the text wraps to the width, and the view keeps whatever height layout gave
|
|
571
|
+
* it.
|
|
572
|
+
*/
|
|
573
|
+
- (void)uikitCompat_configureForTextInput
|
|
574
|
+
{
|
|
575
|
+
self.drawsBackground = NO;
|
|
576
|
+
self.richText = NO;
|
|
577
|
+
self.importsGraphics = NO;
|
|
578
|
+
self.allowsUndo = YES;
|
|
579
|
+
self.minSize = NSMakeSize(0, 0);
|
|
580
|
+
self.maxSize = NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX);
|
|
581
|
+
self.verticallyResizable = NO;
|
|
582
|
+
self.horizontallyResizable = NO;
|
|
583
|
+
self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
584
|
+
self.textContainer.widthTracksTextView = YES;
|
|
585
|
+
self.textContainer.heightTracksTextView = YES;
|
|
586
|
+
self.textContainer.lineFragmentPadding = 0;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
- (instancetype)initWithFrame:(NSRect)frame
|
|
590
|
+
{
|
|
591
|
+
if (self = [super initWithFrame:frame]) {
|
|
592
|
+
[self uikitCompat_configureForTextInput];
|
|
593
|
+
}
|
|
594
|
+
return self;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
- (instancetype)initWithFrame:(NSRect)frame textContainer:(NSTextContainer *)container
|
|
598
|
+
{
|
|
599
|
+
if (self = [super initWithFrame:frame textContainer:container]) {
|
|
600
|
+
[self uikitCompat_configureForTextInput];
|
|
601
|
+
}
|
|
602
|
+
return self;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
- (id<UITextViewDelegate>)uikitCompat_uiDelegate
|
|
606
|
+
{
|
|
607
|
+
id delegate = self.delegate;
|
|
608
|
+
return [delegate conformsToProtocol:@protocol(UITextViewDelegate)] ? delegate : nil;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// NSTextView funnels every edit through here, including paste, drops and
|
|
612
|
+
// undo -- which a keystroke-level hook would miss.
|
|
613
|
+
- (void)didChangeText
|
|
614
|
+
{
|
|
615
|
+
[super didChangeText];
|
|
616
|
+
id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
|
|
617
|
+
if ([delegate respondsToSelector:@selector(textViewDidChange:)]) {
|
|
618
|
+
[delegate textViewDidChange:self];
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
- (void)setSelectedRange:(NSRange)range affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelecting
|
|
623
|
+
{
|
|
624
|
+
[super setSelectedRange:range affinity:affinity stillSelecting:stillSelecting];
|
|
625
|
+
id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
|
|
626
|
+
if ([delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) {
|
|
627
|
+
[delegate textViewDidChangeSelection:self];
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// NSTextView is its own responder rather than borrowing the window's field
|
|
632
|
+
// editor, so begin and end editing are the responder transitions themselves.
|
|
633
|
+
- (BOOL)becomeFirstResponder
|
|
634
|
+
{
|
|
635
|
+
if (![super becomeFirstResponder]) {
|
|
636
|
+
return NO;
|
|
637
|
+
}
|
|
638
|
+
id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
|
|
639
|
+
if ([delegate respondsToSelector:@selector(textViewDidBeginEditing:)]) {
|
|
640
|
+
[delegate textViewDidBeginEditing:self];
|
|
641
|
+
}
|
|
642
|
+
return YES;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
- (BOOL)resignFirstResponder
|
|
646
|
+
{
|
|
647
|
+
if (![super resignFirstResponder]) {
|
|
648
|
+
return NO;
|
|
649
|
+
}
|
|
650
|
+
id<UITextViewDelegate> delegate = [self uikitCompat_uiDelegate];
|
|
651
|
+
if ([delegate respondsToSelector:@selector(textViewDidEndEditing:)]) {
|
|
652
|
+
[delegate textViewDidEndEditing:self];
|
|
653
|
+
}
|
|
654
|
+
return YES;
|
|
655
|
+
}
|
|
656
|
+
|
|
357
657
|
@synthesize autocapitalizationType = _autocapitalizationType;
|
|
358
658
|
@synthesize autocorrectionType = _autocorrectionType;
|
|
359
659
|
@synthesize spellCheckingType = _spellCheckingType;
|