react-native-enriched-markdown-scaffold 1.0.2-scaffold.4 → 1.0.2-scaffold.6
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 +15 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +15 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +3 -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 +20 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +4 -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 +22 -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 +22 -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 +68 -0
- package/docs/WEB.md +1 -0
- package/ios/EnrichedMarkdown.mm +188 -2
- package/ios/EnrichedMarkdownText.mm +117 -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 +20 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +4 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
- package/ios/utils/ENRMLinkChip.m +38 -20
- 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/EditMenuUtils.h +3 -0
- package/ios/utils/EditMenuUtils.m +116 -0
- 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 +70 -1
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +70 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/jest/index.js +14 -0
- package/lib/module/jest/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +46 -4
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +63 -2
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +63 -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 +46 -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 +70 -1
- package/src/EnrichedMarkdownTextNativeComponent.ts +70 -1
- package/src/index.tsx +3 -0
- package/src/jest/index.tsx +22 -1
- package/src/native/EnrichedMarkdownText.tsx +87 -9
- package/src/types/MarkdownTextProps.ts +53 -2
- package/src/types/events.ts +38 -0
|
@@ -22,8 +22,10 @@ EnrichedMarkdownProps::EnrichedMarkdownProps(
|
|
|
22
22
|
markdown(convertRawProp(context, rawProps, "markdown", sourceProps.markdown, {})),
|
|
23
23
|
markdownStyle(convertRawProp(context, rawProps, "markdownStyle", sourceProps.markdownStyle, {})),
|
|
24
24
|
emitLinkLayout(convertRawProp(context, rawProps, "emitLinkLayout", sourceProps.emitLinkLayout, {false})),
|
|
25
|
+
emitSelectionChange(convertRawProp(context, rawProps, "emitSelectionChange", sourceProps.emitSelectionChange, {false})),
|
|
25
26
|
enableTaskListItemToggle(convertRawProp(context, rawProps, "enableTaskListItemToggle", sourceProps.enableTaskListItemToggle, {true})),
|
|
26
27
|
enableLinkPreview(convertRawProp(context, rawProps, "enableLinkPreview", sourceProps.enableLinkPreview, {true})),
|
|
28
|
+
linkLongPressSelects(convertRawProp(context, rawProps, "linkLongPressSelects", sourceProps.linkLongPressSelects, {false})),
|
|
27
29
|
selectable(convertRawProp(context, rawProps, "selectable", sourceProps.selectable, {false})),
|
|
28
30
|
selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
|
|
29
31
|
selectionHandleColor(convertRawProp(context, rawProps, "selectionHandleColor", sourceProps.selectionHandleColor, {})),
|
|
@@ -71,6 +73,10 @@ folly::dynamic EnrichedMarkdownProps::getDiffProps(
|
|
|
71
73
|
result["emitLinkLayout"] = emitLinkLayout;
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
if (emitSelectionChange != oldProps->emitSelectionChange) {
|
|
77
|
+
result["emitSelectionChange"] = emitSelectionChange;
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
if (enableTaskListItemToggle != oldProps->enableTaskListItemToggle) {
|
|
75
81
|
result["enableTaskListItemToggle"] = enableTaskListItemToggle;
|
|
76
82
|
}
|
|
@@ -79,6 +85,10 @@ folly::dynamic EnrichedMarkdownProps::getDiffProps(
|
|
|
79
85
|
result["enableLinkPreview"] = enableLinkPreview;
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
if (linkLongPressSelects != oldProps->linkLongPressSelects) {
|
|
89
|
+
result["linkLongPressSelects"] = linkLongPressSelects;
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
if (selectable != oldProps->selectable) {
|
|
83
93
|
result["selectable"] = selectable;
|
|
84
94
|
}
|
|
@@ -310,8 +320,10 @@ EnrichedMarkdownTextProps::EnrichedMarkdownTextProps(
|
|
|
310
320
|
markdown(convertRawProp(context, rawProps, "markdown", sourceProps.markdown, {})),
|
|
311
321
|
markdownStyle(convertRawProp(context, rawProps, "markdownStyle", sourceProps.markdownStyle, {})),
|
|
312
322
|
emitLinkLayout(convertRawProp(context, rawProps, "emitLinkLayout", sourceProps.emitLinkLayout, {false})),
|
|
323
|
+
emitSelectionChange(convertRawProp(context, rawProps, "emitSelectionChange", sourceProps.emitSelectionChange, {false})),
|
|
313
324
|
enableTaskListItemToggle(convertRawProp(context, rawProps, "enableTaskListItemToggle", sourceProps.enableTaskListItemToggle, {true})),
|
|
314
325
|
enableLinkPreview(convertRawProp(context, rawProps, "enableLinkPreview", sourceProps.enableLinkPreview, {true})),
|
|
326
|
+
linkLongPressSelects(convertRawProp(context, rawProps, "linkLongPressSelects", sourceProps.linkLongPressSelects, {false})),
|
|
315
327
|
selectable(convertRawProp(context, rawProps, "selectable", sourceProps.selectable, {false})),
|
|
316
328
|
selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
|
|
317
329
|
selectionHandleColor(convertRawProp(context, rawProps, "selectionHandleColor", sourceProps.selectionHandleColor, {})),
|
|
@@ -359,6 +371,10 @@ folly::dynamic EnrichedMarkdownTextProps::getDiffProps(
|
|
|
359
371
|
result["emitLinkLayout"] = emitLinkLayout;
|
|
360
372
|
}
|
|
361
373
|
|
|
374
|
+
if (emitSelectionChange != oldProps->emitSelectionChange) {
|
|
375
|
+
result["emitSelectionChange"] = emitSelectionChange;
|
|
376
|
+
}
|
|
377
|
+
|
|
362
378
|
if (enableTaskListItemToggle != oldProps->enableTaskListItemToggle) {
|
|
363
379
|
result["enableTaskListItemToggle"] = enableTaskListItemToggle;
|
|
364
380
|
}
|
|
@@ -367,6 +383,10 @@ folly::dynamic EnrichedMarkdownTextProps::getDiffProps(
|
|
|
367
383
|
result["enableLinkPreview"] = enableLinkPreview;
|
|
368
384
|
}
|
|
369
385
|
|
|
386
|
+
if (linkLongPressSelects != oldProps->linkLongPressSelects) {
|
|
387
|
+
result["linkLongPressSelects"] = linkLongPressSelects;
|
|
388
|
+
}
|
|
389
|
+
|
|
370
390
|
if (selectable != oldProps->selectable) {
|
|
371
391
|
result["selectable"] = selectable;
|
|
372
392
|
}
|
|
@@ -2887,8 +2887,10 @@ class EnrichedMarkdownProps final : public ViewProps {
|
|
|
2887
2887
|
std::string markdown{};
|
|
2888
2888
|
EnrichedMarkdownMarkdownStyleStruct markdownStyle{};
|
|
2889
2889
|
bool emitLinkLayout{false};
|
|
2890
|
+
bool emitSelectionChange{false};
|
|
2890
2891
|
bool enableTaskListItemToggle{true};
|
|
2891
2892
|
bool enableLinkPreview{true};
|
|
2893
|
+
bool linkLongPressSelects{false};
|
|
2892
2894
|
bool selectable{false};
|
|
2893
2895
|
SharedColor selectionColor{};
|
|
2894
2896
|
SharedColor selectionHandleColor{};
|
|
@@ -6931,8 +6933,10 @@ class EnrichedMarkdownTextProps final : public ViewProps {
|
|
|
6931
6933
|
std::string markdown{};
|
|
6932
6934
|
EnrichedMarkdownTextMarkdownStyleStruct markdownStyle{};
|
|
6933
6935
|
bool emitLinkLayout{false};
|
|
6936
|
+
bool emitSelectionChange{false};
|
|
6934
6937
|
bool enableTaskListItemToggle{true};
|
|
6935
6938
|
bool enableLinkPreview{true};
|
|
6939
|
+
bool linkLongPressSelects{false};
|
|
6936
6940
|
bool selectable{false};
|
|
6937
6941
|
SharedColor selectionColor{};
|
|
6938
6942
|
SharedColor selectionHandleColor{};
|
|
@@ -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
|
@@ -134,11 +134,31 @@ void ENRMEnumerateLinkChipFragments(NSLayoutManager *layoutManager, NSTextContai
|
|
|
134
134
|
|
|
135
135
|
CGRect textRect = [layoutManager boundingRectForGlyphRange:intersect
|
|
136
136
|
inTextContainer:textContainer];
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
NSRange fragmentChars = [layoutManager characterRangeForGlyphRange:intersect
|
|
138
|
+
actualGlyphRange:NULL];
|
|
139
|
+
NSUInteger lineEnd = NSMaxRange(lineRange);
|
|
140
|
+
CGFloat left =
|
|
141
|
+
lineRect.origin.x + [layoutManager locationForGlyphAtIndex:intersect.location].x;
|
|
142
|
+
|
|
143
|
+
NSUInteger visibleCharEnd = NSMaxRange(fragmentChars);
|
|
144
|
+
NSString *chars = storage.string;
|
|
145
|
+
NSCharacterSet *breakable = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
146
|
+
while (visibleCharEnd > fragmentChars.location &&
|
|
147
|
+
[chars characterAtIndex:visibleCharEnd - 1] != kSpacerChar &&
|
|
148
|
+
[breakable characterIsMember:[chars characterAtIndex:visibleCharEnd - 1]]) {
|
|
149
|
+
visibleCharEnd--;
|
|
150
|
+
}
|
|
151
|
+
NSUInteger visibleGlyphEnd = NSMaxRange(intersect);
|
|
152
|
+
if (visibleCharEnd < NSMaxRange(fragmentChars)) {
|
|
153
|
+
visibleGlyphEnd = [layoutManager glyphIndexForCharacterAtIndex:visibleCharEnd];
|
|
154
|
+
}
|
|
139
155
|
|
|
140
|
-
|
|
141
|
-
|
|
156
|
+
CGFloat right;
|
|
157
|
+
if (visibleGlyphEnd < lineEnd) {
|
|
158
|
+
right = lineRect.origin.x + [layoutManager locationForGlyphAtIndex:visibleGlyphEnd].x;
|
|
159
|
+
} else {
|
|
160
|
+
right = CGRectGetMaxX(usedRect);
|
|
161
|
+
}
|
|
142
162
|
if (isLast && paddingHorizontal > 0) {
|
|
143
163
|
NSUInteger lastGlyph = NSMaxRange(intersect) - 1;
|
|
144
164
|
CGFloat spacerX =
|
|
@@ -146,8 +166,6 @@ void ENRMEnumerateLinkChipFragments(NSLayoutManager *layoutManager, NSTextContai
|
|
|
146
166
|
right = MAX(right, spacerX + paddingHorizontal);
|
|
147
167
|
}
|
|
148
168
|
|
|
149
|
-
NSRange fragmentChars = [layoutManager characterRangeForGlyphRange:intersect
|
|
150
|
-
actualGlyphRange:NULL];
|
|
151
169
|
UIFont *font = chipFontInRange(storage, fragmentChars);
|
|
152
170
|
CGFloat top = CGRectGetMinY(textRect);
|
|
153
171
|
CGFloat bottom = CGRectGetMaxY(textRect);
|
|
@@ -177,18 +195,18 @@ void ENRMDrawLinkChipsForGlyphRange(NSLayoutManager *layoutManager, NSTextContai
|
|
|
177
195
|
|
|
178
196
|
// Enumerate the whole storage (like CodeBackground) so a chip that starts
|
|
179
197
|
// before the dirty range still gets its full rect.
|
|
180
|
-
[storage
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
[storage
|
|
199
|
+
enumerateAttribute:ENRMLinkChipAttributeName
|
|
200
|
+
inRange:NSMakeRange(0, storage.length)
|
|
201
|
+
options:0
|
|
202
|
+
usingBlock:^(ENRMLinkChip *chip, NSRange range, BOOL *stop) {
|
|
203
|
+
if (!chip.isVisible || NSIntersectionRange(range, charRange).length == 0)
|
|
204
|
+
return;
|
|
205
|
+
|
|
206
|
+
ENRMEnumerateLinkChipFragments(
|
|
207
|
+
layoutManager, textContainer, range, chip, ^(CGRect rect, BOOL isFirst, BOOL isLast) {
|
|
208
|
+
ENRMDrawChipFragment(CGRectOffset(rect, origin.x, origin.y), chip.backgroundColor,
|
|
209
|
+
chip.borderColor, chip.borderWidth, chip.borderRadius, isFirst, isLast, YES);
|
|
210
|
+
});
|
|
211
|
+
}];
|
|
194
212
|
}
|
|
@@ -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
|
|
@@ -117,6 +117,9 @@ UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange ra
|
|
|
117
117
|
NSArray<UIAction *> *_Nullable customActions,
|
|
118
118
|
ENRMSelectionMenuConfig selectionMenuConfig,
|
|
119
119
|
ENRMCopyRequestHandler _Nullable onCopyRequest) API_AVAILABLE(ios(16.0));
|
|
120
|
+
|
|
121
|
+
void ENRMSelectRangeAndPresentEditMenu(UITextView *textView, NSRange range);
|
|
122
|
+
void ENRMDismissSelectionEditMenuIfMoved(UITextView *textView);
|
|
120
123
|
#else
|
|
121
124
|
NSMenu *_Nullable buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range,
|
|
122
125
|
NSString *_Nullable cachedMarkdown, StyleConfig *styleConfig,
|