react-native-enriched-markdown-scaffold 1.0.2-scaffold.4 → 1.0.2-scaffold.5
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.
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +12 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +2 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +12 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +2 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +74 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +68 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +2 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +107 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +48 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +42 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +230 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +11 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +8 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +8 -1
- package/docs/API_REFERENCE.md +58 -0
- package/ios/EnrichedMarkdown.mm +174 -2
- package/ios/EnrichedMarkdownText.mm +103 -1
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +74 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +68 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +2 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
- package/ios/utils/ENRMLinkChip.m +14 -14
- package/ios/utils/ENRMLinkLayout.m +12 -12
- package/ios/utils/ENRMSelectionChange.h +96 -0
- package/ios/utils/ENRMSelectionChange.m +204 -0
- package/ios/utils/EditMenuUtils+macOS.m +1 -1
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +1 -2
- package/ios/views/ENRMCodeBlockContainerView.h +3 -0
- package/ios/views/ENRMCodeBlockContainerView.m +3 -0
- package/ios/views/ENRMMathContainerView.h +3 -0
- package/ios/views/ENRMMathContainerView.m +3 -0
- package/ios/views/TableContainerView.h +3 -0
- package/ios/views/TableContainerView.m +4 -1
- package/lib/module/EnrichedMarkdownNativeComponent.ts +60 -1
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +60 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/jest/index.js +13 -0
- package/lib/module/jest/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +44 -4
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +53 -2
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +53 -2
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/jest/index.d.ts +1 -1
- package/lib/typescript/src/jest/index.d.ts.map +1 -1
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +36 -3
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +36 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownNativeComponent.ts +60 -1
- package/src/EnrichedMarkdownTextNativeComponent.ts +60 -1
- package/src/index.tsx +3 -0
- package/src/jest/index.tsx +21 -1
- package/src/native/EnrichedMarkdownText.tsx +85 -9
- package/src/types/MarkdownTextProps.ts +43 -2
- package/src/types/events.ts +38 -0
|
@@ -14,9 +14,33 @@
|
|
|
14
14
|
NS_ASSUME_NONNULL_BEGIN
|
|
15
15
|
|
|
16
16
|
@protocol RCTEnrichedMarkdownViewProtocol <NSObject>
|
|
17
|
-
|
|
17
|
+
- (void)clearSelection;
|
|
18
18
|
@end
|
|
19
19
|
|
|
20
|
+
RCT_EXTERN inline void RCTEnrichedMarkdownHandleCommand(
|
|
21
|
+
id<RCTEnrichedMarkdownViewProtocol> componentView,
|
|
22
|
+
NSString const *commandName,
|
|
23
|
+
NSArray const *args)
|
|
24
|
+
{
|
|
25
|
+
if ([commandName isEqualToString:@"clearSelection"]) {
|
|
26
|
+
#if RCT_DEBUG
|
|
27
|
+
if ([args count] != 0) {
|
|
28
|
+
RCTLogError(@"%@ command %@ received %d arguments, expected %d.", @"EnrichedMarkdown", commandName, (int)[args count], 0);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
#endif
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
[componentView clearSelection];
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#if RCT_DEBUG
|
|
40
|
+
RCTLogError(@"%@ received command %@, which is not a supported command.", @"EnrichedMarkdown", commandName);
|
|
41
|
+
#endif
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
@protocol RCTEnrichedMarkdownTextInputViewProtocol <NSObject>
|
|
21
45
|
- (void)focus;
|
|
22
46
|
- (void)blur;
|
|
@@ -576,7 +600,31 @@ if ([commandName isEqualToString:@"requestCaretRect"]) {
|
|
|
576
600
|
}
|
|
577
601
|
|
|
578
602
|
@protocol RCTEnrichedMarkdownTextViewProtocol <NSObject>
|
|
579
|
-
|
|
603
|
+
- (void)clearSelection;
|
|
580
604
|
@end
|
|
581
605
|
|
|
606
|
+
RCT_EXTERN inline void RCTEnrichedMarkdownTextHandleCommand(
|
|
607
|
+
id<RCTEnrichedMarkdownTextViewProtocol> componentView,
|
|
608
|
+
NSString const *commandName,
|
|
609
|
+
NSArray const *args)
|
|
610
|
+
{
|
|
611
|
+
if ([commandName isEqualToString:@"clearSelection"]) {
|
|
612
|
+
#if RCT_DEBUG
|
|
613
|
+
if ([args count] != 0) {
|
|
614
|
+
RCTLogError(@"%@ command %@ received %d arguments, expected %d.", @"EnrichedMarkdownText", commandName, (int)[args count], 0);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
#endif
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
[componentView clearSelection];
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#if RCT_DEBUG
|
|
626
|
+
RCTLogError(@"%@ received command %@, which is not a supported command.", @"EnrichedMarkdownText", commandName);
|
|
627
|
+
#endif
|
|
628
|
+
}
|
|
629
|
+
|
|
582
630
|
NS_ASSUME_NONNULL_END
|
package/ios/utils/ENRMLinkChip.m
CHANGED
|
@@ -177,18 +177,18 @@ void ENRMDrawLinkChipsForGlyphRange(NSLayoutManager *layoutManager, NSTextContai
|
|
|
177
177
|
|
|
178
178
|
// Enumerate the whole storage (like CodeBackground) so a chip that starts
|
|
179
179
|
// before the dirty range still gets its full rect.
|
|
180
|
-
[storage
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
180
|
+
[storage
|
|
181
|
+
enumerateAttribute:ENRMLinkChipAttributeName
|
|
182
|
+
inRange:NSMakeRange(0, storage.length)
|
|
183
|
+
options:0
|
|
184
|
+
usingBlock:^(ENRMLinkChip *chip, NSRange range, BOOL *stop) {
|
|
185
|
+
if (!chip.isVisible || NSIntersectionRange(range, charRange).length == 0)
|
|
186
|
+
return;
|
|
187
|
+
|
|
188
|
+
ENRMEnumerateLinkChipFragments(
|
|
189
|
+
layoutManager, textContainer, range, chip, ^(CGRect rect, BOOL isFirst, BOOL isLast) {
|
|
190
|
+
ENRMDrawChipFragment(CGRectOffset(rect, origin.x, origin.y), chip.backgroundColor,
|
|
191
|
+
chip.borderColor, chip.borderWidth, chip.borderRadius, isFirst, isLast, YES);
|
|
192
|
+
});
|
|
193
|
+
}];
|
|
194
194
|
}
|
|
@@ -24,18 +24,18 @@ NSArray<NSDictionary *> *ENRMCollectLinkLayoutFragments(ENRMPlatformTextView *te
|
|
|
24
24
|
NSString *text = ENRMPlainTextByRemovingLinkSpacers(storage, range);
|
|
25
25
|
NSNumber *index = @(linkIndex++);
|
|
26
26
|
|
|
27
|
-
ENRMEnumerateLinkChipFragments(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
ENRMEnumerateLinkChipFragments(layoutManager, textContainer, range, chip,
|
|
28
|
+
^(CGRect rect, BOOL isFirst, BOOL isLast) {
|
|
29
|
+
[fragments addObject:@{
|
|
30
|
+
@"linkIndex" : index,
|
|
31
|
+
@"url" : url ?: @"",
|
|
32
|
+
@"text" : text,
|
|
33
|
+
@"x" : @(rect.origin.x + offset.x),
|
|
34
|
+
@"y" : @(rect.origin.y + offset.y),
|
|
35
|
+
@"width" : @(rect.size.width),
|
|
36
|
+
@"height" : @(rect.size.height),
|
|
37
|
+
}];
|
|
38
|
+
});
|
|
39
39
|
}];
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#import "ENRMUIKit.h"
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
/// Keys of an `onSelectionChange` payload. Rects are `NSValue`-wrapped CGRects in
|
|
7
|
+
/// the reporting view's coordinates, so a payload compares with `-isEqual:` for
|
|
8
|
+
/// deduplication.
|
|
9
|
+
extern NSString *const ENRMSelectionActiveKey;
|
|
10
|
+
extern NSString *const ENRMSelectionSourceKey;
|
|
11
|
+
extern NSString *const ENRMSelectionTextKey;
|
|
12
|
+
extern NSString *const ENRMSelectionMarkdownKey;
|
|
13
|
+
extern NSString *const ENRMSelectionWholeKey;
|
|
14
|
+
extern NSString *const ENRMSelectionRectKey;
|
|
15
|
+
extern NSString *const ENRMSelectionStartRectKey;
|
|
16
|
+
extern NSString *const ENRMSelectionEndRectKey;
|
|
17
|
+
/// Only in `ENRMTextSelectionInTextView` results: whether the range spans the
|
|
18
|
+
/// whole text (same threshold `markdownForRange` uses for the cached markdown).
|
|
19
|
+
extern NSString *const ENRMSelectionFullKey;
|
|
20
|
+
|
|
21
|
+
#ifdef __cplusplus
|
|
22
|
+
extern "C" {
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
/// The payload reported once the selection is gone.
|
|
26
|
+
NSDictionary *ENRMInactiveSelectionPayload(void);
|
|
27
|
+
|
|
28
|
+
/// A long-pressed block (table / math / code): `whole`, every rect is `frame`.
|
|
29
|
+
NSDictionary *ENRMBlockSelectionPayload(NSString *source, NSString *markdown, NSString *text, CGRect frame);
|
|
30
|
+
|
|
31
|
+
#if !TARGET_OS_OSX
|
|
32
|
+
/// Text, full-selection flag and rects of `textView`'s selection, converted to
|
|
33
|
+
/// `targetView`'s coordinates; nil when nothing is selected. `rect` is the union
|
|
34
|
+
/// of the selection rects, `startRect` / `endRect` the rects holding its start
|
|
35
|
+
/// and end. The owner adds source, markdown and whole to build the payload.
|
|
36
|
+
NSDictionary *_Nullable ENRMTextSelectionInTextView(UITextView *textView, UIView *targetView);
|
|
37
|
+
#endif
|
|
38
|
+
|
|
39
|
+
#ifdef __cplusplus
|
|
40
|
+
}
|
|
41
|
+
#endif
|
|
42
|
+
|
|
43
|
+
/// Coalesces selection changes into at most one report per run loop turn. The
|
|
44
|
+
/// payload is read when the turn fires, so the latest state always reaches JS;
|
|
45
|
+
/// a cleared selection is reported right away and drops any pending update.
|
|
46
|
+
/// Identical payloads are sent once.
|
|
47
|
+
@interface ENRMSelectionChangeReporter : NSObject
|
|
48
|
+
|
|
49
|
+
- (instancetype)initWithSnapshot:(NSDictionary * (^)(void))snapshot
|
|
50
|
+
emit:(void (^)(NSDictionary *payload))emit NS_DESIGNATED_INITIALIZER;
|
|
51
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
52
|
+
|
|
53
|
+
/// Off by default. Toggling resets the last reported payload to inactive.
|
|
54
|
+
@property (nonatomic, assign) BOOL enabled;
|
|
55
|
+
|
|
56
|
+
- (void)schedule;
|
|
57
|
+
- (void)selectionChanged:(BOOL)hasSelection;
|
|
58
|
+
- (void)flushNow;
|
|
59
|
+
- (void)cancel;
|
|
60
|
+
/// Makes the next report go out even if it equals the last one — a block
|
|
61
|
+
/// long-pressed again is a new request for the app's menu, not a repeat.
|
|
62
|
+
- (void)forgetLastReported;
|
|
63
|
+
|
|
64
|
+
@end
|
|
65
|
+
|
|
66
|
+
#ifdef __cplusplus
|
|
67
|
+
/// Converts a payload into a codegen `OnSelectionChange` struct. Templated
|
|
68
|
+
/// because each component has its own (structurally identical) event struct.
|
|
69
|
+
template <typename Event> inline Event ENRMSelectionChangeEventFromPayload(NSDictionary *payload)
|
|
70
|
+
{
|
|
71
|
+
Event event;
|
|
72
|
+
event.active = [payload[ENRMSelectionActiveKey] boolValue];
|
|
73
|
+
event.source = std::string([payload[ENRMSelectionSourceKey] UTF8String] ?: "selection");
|
|
74
|
+
event.text = std::string([payload[ENRMSelectionTextKey] UTF8String] ?: "");
|
|
75
|
+
event.markdown = std::string([payload[ENRMSelectionMarkdownKey] UTF8String] ?: "");
|
|
76
|
+
event.whole = [payload[ENRMSelectionWholeKey] boolValue];
|
|
77
|
+
|
|
78
|
+
auto assign = [](auto &target, NSValue *value) {
|
|
79
|
+
#if TARGET_OS_OSX
|
|
80
|
+
CGRect rect = value ? NSRectToCGRect(value.rectValue) : CGRectZero;
|
|
81
|
+
#else
|
|
82
|
+
CGRect rect = value ? value.CGRectValue : CGRectZero;
|
|
83
|
+
#endif
|
|
84
|
+
target.x = rect.origin.x;
|
|
85
|
+
target.y = rect.origin.y;
|
|
86
|
+
target.width = rect.size.width;
|
|
87
|
+
target.height = rect.size.height;
|
|
88
|
+
};
|
|
89
|
+
assign(event.rect, payload[ENRMSelectionRectKey]);
|
|
90
|
+
assign(event.startRect, payload[ENRMSelectionStartRectKey]);
|
|
91
|
+
assign(event.endRect, payload[ENRMSelectionEndRectKey]);
|
|
92
|
+
return event;
|
|
93
|
+
}
|
|
94
|
+
#endif
|
|
95
|
+
|
|
96
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#import "ENRMSelectionChange.h"
|
|
2
|
+
#import "ENRMLinkChip.h"
|
|
3
|
+
|
|
4
|
+
NSString *const ENRMSelectionActiveKey = @"active";
|
|
5
|
+
NSString *const ENRMSelectionSourceKey = @"source";
|
|
6
|
+
NSString *const ENRMSelectionTextKey = @"text";
|
|
7
|
+
NSString *const ENRMSelectionMarkdownKey = @"markdown";
|
|
8
|
+
NSString *const ENRMSelectionWholeKey = @"whole";
|
|
9
|
+
NSString *const ENRMSelectionRectKey = @"rect";
|
|
10
|
+
NSString *const ENRMSelectionStartRectKey = @"startRect";
|
|
11
|
+
NSString *const ENRMSelectionEndRectKey = @"endRect";
|
|
12
|
+
NSString *const ENRMSelectionFullKey = @"full";
|
|
13
|
+
|
|
14
|
+
static NSValue *ENRMRectValue(CGRect rect)
|
|
15
|
+
{
|
|
16
|
+
#if TARGET_OS_OSX
|
|
17
|
+
return [NSValue valueWithRect:NSRectFromCGRect(rect)];
|
|
18
|
+
#else
|
|
19
|
+
return [NSValue valueWithCGRect:rect];
|
|
20
|
+
#endif
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
NSDictionary *ENRMInactiveSelectionPayload(void)
|
|
24
|
+
{
|
|
25
|
+
static NSDictionary *payload;
|
|
26
|
+
static dispatch_once_t onceToken;
|
|
27
|
+
dispatch_once(&onceToken, ^{
|
|
28
|
+
NSValue *zero = ENRMRectValue(CGRectZero);
|
|
29
|
+
payload = @{
|
|
30
|
+
ENRMSelectionActiveKey : @NO,
|
|
31
|
+
ENRMSelectionSourceKey : @"selection",
|
|
32
|
+
ENRMSelectionTextKey : @"",
|
|
33
|
+
ENRMSelectionMarkdownKey : @"",
|
|
34
|
+
ENRMSelectionWholeKey : @NO,
|
|
35
|
+
ENRMSelectionRectKey : zero,
|
|
36
|
+
ENRMSelectionStartRectKey : zero,
|
|
37
|
+
ENRMSelectionEndRectKey : zero,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
return payload;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
NSDictionary *ENRMBlockSelectionPayload(NSString *source, NSString *markdown, NSString *text, CGRect frame)
|
|
44
|
+
{
|
|
45
|
+
NSValue *rect = ENRMRectValue(frame);
|
|
46
|
+
return @{
|
|
47
|
+
ENRMSelectionActiveKey : @YES,
|
|
48
|
+
ENRMSelectionSourceKey : source,
|
|
49
|
+
ENRMSelectionTextKey : text ?: @"",
|
|
50
|
+
ENRMSelectionMarkdownKey : markdown ?: @"",
|
|
51
|
+
ENRMSelectionWholeKey : @YES,
|
|
52
|
+
ENRMSelectionRectKey : rect,
|
|
53
|
+
ENRMSelectionStartRectKey : rect,
|
|
54
|
+
ENRMSelectionEndRectKey : rect,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#if !TARGET_OS_OSX
|
|
59
|
+
NSDictionary *ENRMTextSelectionInTextView(UITextView *textView, UIView *targetView)
|
|
60
|
+
{
|
|
61
|
+
NSRange range = textView.selectedRange;
|
|
62
|
+
UITextRange *textRange = textView.selectedTextRange;
|
|
63
|
+
if (range.location == NSNotFound || range.length == 0 || textRange == nil || textRange.isEmpty) {
|
|
64
|
+
return nil;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
NSAttributedString *attributedText = textView.attributedText;
|
|
68
|
+
if (NSMaxRange(range) > attributedText.length) {
|
|
69
|
+
return nil;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
CGRect unionRect = CGRectNull;
|
|
73
|
+
CGRect startRect = CGRectNull;
|
|
74
|
+
CGRect endRect = CGRectNull;
|
|
75
|
+
CGRect firstRect = CGRectNull;
|
|
76
|
+
CGRect lastRect = CGRectNull;
|
|
77
|
+
for (UITextSelectionRect *selectionRect in [textView selectionRectsForRange:textRange]) {
|
|
78
|
+
CGRect rect = selectionRect.rect;
|
|
79
|
+
if (CGRectIsNull(rect) || CGRectIsInfinite(rect) || CGRectIsEmpty(rect)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
unionRect = CGRectUnion(unionRect, rect);
|
|
83
|
+
if (CGRectIsNull(firstRect)) {
|
|
84
|
+
firstRect = rect;
|
|
85
|
+
}
|
|
86
|
+
lastRect = rect;
|
|
87
|
+
if (selectionRect.containsStart && CGRectIsNull(startRect)) {
|
|
88
|
+
startRect = rect;
|
|
89
|
+
}
|
|
90
|
+
if (selectionRect.containsEnd) {
|
|
91
|
+
endRect = rect;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Not laid out yet: report once layout gives the range a position.
|
|
95
|
+
if (CGRectIsNull(unionRect)) {
|
|
96
|
+
return nil;
|
|
97
|
+
}
|
|
98
|
+
if (CGRectIsNull(startRect)) {
|
|
99
|
+
startRect = firstRect;
|
|
100
|
+
}
|
|
101
|
+
if (CGRectIsNull(endRect)) {
|
|
102
|
+
endRect = lastRect;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return @{
|
|
106
|
+
ENRMSelectionTextKey : ENRMPlainTextByRemovingLinkSpacers(attributedText, range),
|
|
107
|
+
ENRMSelectionFullKey : @(range.location == 0 && range.length >= attributedText.length - 1),
|
|
108
|
+
ENRMSelectionRectKey : ENRMRectValue([textView convertRect:unionRect toView:targetView]),
|
|
109
|
+
ENRMSelectionStartRectKey : ENRMRectValue([textView convertRect:startRect toView:targetView]),
|
|
110
|
+
ENRMSelectionEndRectKey : ENRMRectValue([textView convertRect:endRect toView:targetView]),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
#endif
|
|
114
|
+
|
|
115
|
+
@implementation ENRMSelectionChangeReporter {
|
|
116
|
+
NSDictionary * (^_snapshot)(void);
|
|
117
|
+
void (^_emit)(NSDictionary *);
|
|
118
|
+
NSDictionary *_lastReported;
|
|
119
|
+
BOOL _pending;
|
|
120
|
+
// Bumped on cancel so a scheduled block that already left the queue is a no-op.
|
|
121
|
+
NSUInteger _generation;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
- (instancetype)initWithSnapshot:(NSDictionary * (^)(void))snapshot emit:(void (^)(NSDictionary *))emit
|
|
125
|
+
{
|
|
126
|
+
if (self = [super init]) {
|
|
127
|
+
_snapshot = [snapshot copy];
|
|
128
|
+
_emit = [emit copy];
|
|
129
|
+
_lastReported = ENRMInactiveSelectionPayload();
|
|
130
|
+
}
|
|
131
|
+
return self;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
- (void)setEnabled:(BOOL)enabled
|
|
135
|
+
{
|
|
136
|
+
if (_enabled == enabled) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
_enabled = enabled;
|
|
140
|
+
[self cancel];
|
|
141
|
+
_lastReported = ENRMInactiveSelectionPayload();
|
|
142
|
+
if (enabled) {
|
|
143
|
+
[self schedule];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
- (void)schedule
|
|
148
|
+
{
|
|
149
|
+
if (!_enabled || _pending) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
_pending = YES;
|
|
153
|
+
NSUInteger generation = _generation;
|
|
154
|
+
__weak ENRMSelectionChangeReporter *weakSelf = self;
|
|
155
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
156
|
+
ENRMSelectionChangeReporter *strongSelf = weakSelf;
|
|
157
|
+
if (!strongSelf || !strongSelf->_pending || strongSelf->_generation != generation) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
strongSelf->_pending = NO;
|
|
161
|
+
[strongSelf report];
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
- (void)selectionChanged:(BOOL)hasSelection
|
|
166
|
+
{
|
|
167
|
+
if (hasSelection) {
|
|
168
|
+
[self schedule];
|
|
169
|
+
} else {
|
|
170
|
+
[self flushNow];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
- (void)flushNow
|
|
175
|
+
{
|
|
176
|
+
[self cancel];
|
|
177
|
+
[self report];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
- (void)forgetLastReported
|
|
181
|
+
{
|
|
182
|
+
_lastReported = ENRMInactiveSelectionPayload();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
- (void)cancel
|
|
186
|
+
{
|
|
187
|
+
_pending = NO;
|
|
188
|
+
_generation++;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
- (void)report
|
|
192
|
+
{
|
|
193
|
+
if (!_enabled) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
NSDictionary *current = _snapshot();
|
|
197
|
+
if ([current isEqualToDictionary:_lastReported]) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
_lastReported = current;
|
|
201
|
+
_emit(current);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@end
|
|
@@ -115,8 +115,7 @@ static const CGFloat kFocusRectPadding = 2.0;
|
|
|
115
115
|
|
|
116
116
|
BOOL isImg = item[@"altText"] != nil;
|
|
117
117
|
// Link chips pad their text with spacer characters; keep them out of the spoken label.
|
|
118
|
-
NSString *label = isImg ? item[@"altText"]
|
|
119
|
-
: ENRMPlainTextByRemovingLinkSpacers(textView.attributedText, itemRange);
|
|
118
|
+
NSString *label = isImg ? item[@"altText"] : ENRMPlainTextByRemovingLinkSpacers(textView.attributedText, itemRange);
|
|
120
119
|
[elements addObject:[self createElementForRange:itemRange
|
|
121
120
|
type:isImg ? ElementTypeImage : ElementTypeLink
|
|
122
121
|
text:label
|
|
@@ -45,6 +45,9 @@ typedef void (^ENRMCodeBlockCopyBlock)(NSString *code, NSString *language);
|
|
|
45
45
|
/// Set by the host; receives the Copy item's content when
|
|
46
46
|
/// `menuOptions.interceptCopy` is set.
|
|
47
47
|
@property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
|
|
48
|
+
/// Called instead of the menu on long press while `contextMenuHidden` is on, so
|
|
49
|
+
/// the owner can report the block (`onSelectionChange`) and show its own UI.
|
|
50
|
+
@property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onMenuSuppressed;
|
|
48
51
|
|
|
49
52
|
// Fired when the code is copied (header button, context-menu Copy, or the
|
|
50
53
|
// VoiceOver copy action); set by the host to bridge up to the JS onCopyPress
|
|
@@ -495,6 +495,9 @@ static BOOL ENRMColorIsDark(RCTUIColor *color)
|
|
|
495
495
|
{
|
|
496
496
|
// Returning nil is the UIKit-sanctioned way to suppress a context menu.
|
|
497
497
|
if (_pending || self.contextMenuHidden) {
|
|
498
|
+
if (!_pending && self.onMenuSuppressed) {
|
|
499
|
+
self.onMenuSuppressed([self fencedMarkdown], _cachedCode ?: @"");
|
|
500
|
+
}
|
|
498
501
|
return nil;
|
|
499
502
|
}
|
|
500
503
|
__weak __typeof(self) weakSelf = self;
|
|
@@ -42,6 +42,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
42
42
|
/// Set by the host; receives the Copy item's content when
|
|
43
43
|
/// `menuOptions.interceptCopy` is set.
|
|
44
44
|
@property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
|
|
45
|
+
/// Called instead of the menu on long press while `contextMenuHidden` is on, so
|
|
46
|
+
/// the owner can report the block (`onSelectionChange`) and show its own UI.
|
|
47
|
+
@property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onMenuSuppressed;
|
|
45
48
|
|
|
46
49
|
@end
|
|
47
50
|
|
|
@@ -128,6 +128,9 @@
|
|
|
128
128
|
{
|
|
129
129
|
// Returning nil is the UIKit-sanctioned way to suppress a context menu.
|
|
130
130
|
if (self.contextMenuHidden) {
|
|
131
|
+
if (self.onMenuSuppressed) {
|
|
132
|
+
self.onMenuSuppressed([NSString stringWithFormat:@"$$\n%@\n$$", _cachedLatex ?: @""], _cachedLatex ?: @"");
|
|
133
|
+
}
|
|
131
134
|
return nil;
|
|
132
135
|
}
|
|
133
136
|
__weak __typeof(self) weakSelf = self;
|
|
@@ -63,6 +63,9 @@ typedef void (^TableLinkPressBlock)(NSString *url);
|
|
|
63
63
|
/// Set by the host; receives the Copy item's content when
|
|
64
64
|
/// `menuOptions.interceptCopy` is set.
|
|
65
65
|
@property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
|
|
66
|
+
/// Called instead of the menu on long press while `contextMenuHidden` is on, so
|
|
67
|
+
/// the owner can report the block (`onSelectionChange`) and show its own UI.
|
|
68
|
+
@property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onMenuSuppressed;
|
|
66
69
|
|
|
67
70
|
@property (nonatomic, readonly) NSUInteger rowCount;
|
|
68
71
|
/// Number of links across all cells. Tables are not reported by `onLinkLayout`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#import "TableContainerView.h"
|
|
2
|
-
#import "ENRMLinkChip.h"
|
|
3
2
|
#import "AttributedRenderer.h"
|
|
4
3
|
#import "ENRMAccessibilityLabels.h"
|
|
4
|
+
#import "ENRMLinkChip.h"
|
|
5
5
|
#import "HTMLGenerator.h"
|
|
6
6
|
#import "LinkTapUtils.h"
|
|
7
7
|
#import "MarkdownASTNode.h"
|
|
@@ -486,6 +486,9 @@ static void ENRMTableComputeLayout(NSArray<NSArray<TableCellData *> *> *rows, NS
|
|
|
486
486
|
{
|
|
487
487
|
// Returning nil is the UIKit-sanctioned way to suppress a context menu.
|
|
488
488
|
if (self.contextMenuHidden) {
|
|
489
|
+
if (self.onMenuSuppressed) {
|
|
490
|
+
self.onMenuSuppressed(_cachedMarkdown ?: @"", [self buildPlainTextFromRows] ?: @"");
|
|
491
|
+
}
|
|
489
492
|
return nil;
|
|
490
493
|
}
|
|
491
494
|
__weak __typeof(self) weakSelf = self;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
codegenNativeComponent,
|
|
3
|
+
codegenNativeCommands,
|
|
3
4
|
type ViewProps,
|
|
4
5
|
type CodegenTypes,
|
|
5
6
|
type ColorValue,
|
|
7
|
+
type HostComponent,
|
|
6
8
|
} from 'react-native';
|
|
9
|
+
import type React from 'react';
|
|
7
10
|
|
|
8
11
|
// All block styles extend this interface
|
|
9
12
|
interface BaseBlockStyleInternal {
|
|
@@ -274,6 +277,39 @@ export interface LinkLayoutEvent {
|
|
|
274
277
|
}[];
|
|
275
278
|
}
|
|
276
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Current text selection, or a long-pressed table / math / code block while
|
|
282
|
+
* `contextMenuHidden` is on. `active: false` means the selection was cleared
|
|
283
|
+
* (every other field is then empty / zero). Rects are in dp / pt, relative to
|
|
284
|
+
* the native view's origin; rect objects are written inline because codegen
|
|
285
|
+
* only accepts inline object literals in event payloads.
|
|
286
|
+
*/
|
|
287
|
+
export interface SelectionChangeEvent {
|
|
288
|
+
active: boolean;
|
|
289
|
+
source: string;
|
|
290
|
+
text: string;
|
|
291
|
+
markdown: string;
|
|
292
|
+
whole: boolean;
|
|
293
|
+
rect: {
|
|
294
|
+
x: CodegenTypes.Double;
|
|
295
|
+
y: CodegenTypes.Double;
|
|
296
|
+
width: CodegenTypes.Double;
|
|
297
|
+
height: CodegenTypes.Double;
|
|
298
|
+
};
|
|
299
|
+
startRect: {
|
|
300
|
+
x: CodegenTypes.Double;
|
|
301
|
+
y: CodegenTypes.Double;
|
|
302
|
+
width: CodegenTypes.Double;
|
|
303
|
+
height: CodegenTypes.Double;
|
|
304
|
+
};
|
|
305
|
+
endRect: {
|
|
306
|
+
x: CodegenTypes.Double;
|
|
307
|
+
y: CodegenTypes.Double;
|
|
308
|
+
width: CodegenTypes.Double;
|
|
309
|
+
height: CodegenTypes.Double;
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
277
313
|
export interface TaskListItemPressEvent {
|
|
278
314
|
index: CodegenTypes.Int32;
|
|
279
315
|
checked: boolean;
|
|
@@ -441,6 +477,18 @@ export interface NativeProps extends ViewProps {
|
|
|
441
477
|
* Deduplicated: not re-fired when the payload is unchanged.
|
|
442
478
|
*/
|
|
443
479
|
onLinkLayout?: CodegenTypes.DirectEventHandler<LinkLayoutEvent>;
|
|
480
|
+
/**
|
|
481
|
+
* When true, the view reports selection changes (and, with
|
|
482
|
+
* `contextMenuHidden`, block long-presses) through `onSelectionChange`.
|
|
483
|
+
* Set by the JS wrapper only when `onSelectionChange` is provided.
|
|
484
|
+
* @default false
|
|
485
|
+
*/
|
|
486
|
+
emitSelectionChange?: CodegenTypes.WithDefault<boolean, false>;
|
|
487
|
+
/**
|
|
488
|
+
* Fired while `emitSelectionChange` is on: coalesced to one event per frame,
|
|
489
|
+
* deduplicated, and `active: false` once the selection is gone.
|
|
490
|
+
*/
|
|
491
|
+
onSelectionChange?: CodegenTypes.DirectEventHandler<SelectionChangeEvent>;
|
|
444
492
|
/**
|
|
445
493
|
* Callback fired when a task list checkbox is tapped.
|
|
446
494
|
* Receives the 0-based task index, current checked state, and the item's plain text.
|
|
@@ -594,6 +642,17 @@ export interface NativeProps extends ViewProps {
|
|
|
594
642
|
writingDirection?: CodegenTypes.WithDefault<string, 'first-strong'>;
|
|
595
643
|
}
|
|
596
644
|
|
|
645
|
+
type ComponentType = HostComponent<NativeProps>;
|
|
646
|
+
|
|
647
|
+
interface NativeCommands {
|
|
648
|
+
/** Clears the text selection (and any active block) and fires `onSelectionChange` with `active: false`. */
|
|
649
|
+
clearSelection: (viewRef: React.ElementRef<ComponentType>) => void;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
653
|
+
supportedCommands: ['clearSelection'],
|
|
654
|
+
});
|
|
655
|
+
|
|
597
656
|
export default codegenNativeComponent<NativeProps>('EnrichedMarkdown', {
|
|
598
657
|
interfaceOnly: true,
|
|
599
|
-
})
|
|
658
|
+
}) as HostComponent<NativeProps>;
|