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.
Files changed (71) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +15 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +3 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +15 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +3 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +74 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +68 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +4 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +107 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +19 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +22 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +48 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +22 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +42 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +14 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +230 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +11 -1
  18. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +8 -1
  19. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +8 -1
  20. package/docs/API_REFERENCE.md +68 -0
  21. package/docs/WEB.md +1 -0
  22. package/ios/EnrichedMarkdown.mm +188 -2
  23. package/ios/EnrichedMarkdownText.mm +117 -1
  24. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +74 -0
  25. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +68 -0
  26. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  27. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +4 -0
  28. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  29. package/ios/utils/ENRMLinkChip.m +38 -20
  30. package/ios/utils/ENRMLinkLayout.m +12 -12
  31. package/ios/utils/ENRMSelectionChange.h +96 -0
  32. package/ios/utils/ENRMSelectionChange.m +204 -0
  33. package/ios/utils/EditMenuUtils+macOS.m +1 -1
  34. package/ios/utils/EditMenuUtils.h +3 -0
  35. package/ios/utils/EditMenuUtils.m +116 -0
  36. package/ios/utils/MarkdownAccessibilityElementBuilder.m +1 -2
  37. package/ios/views/ENRMCodeBlockContainerView.h +3 -0
  38. package/ios/views/ENRMCodeBlockContainerView.m +3 -0
  39. package/ios/views/ENRMMathContainerView.h +3 -0
  40. package/ios/views/ENRMMathContainerView.m +3 -0
  41. package/ios/views/TableContainerView.h +3 -0
  42. package/ios/views/TableContainerView.m +4 -1
  43. package/lib/module/EnrichedMarkdownNativeComponent.ts +70 -1
  44. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +70 -1
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/jest/index.js +14 -0
  47. package/lib/module/jest/index.js.map +1 -1
  48. package/lib/module/native/EnrichedMarkdownText.js +46 -4
  49. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  50. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +63 -2
  51. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  52. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +63 -2
  53. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  54. package/lib/typescript/src/index.d.ts +2 -2
  55. package/lib/typescript/src/index.d.ts.map +1 -1
  56. package/lib/typescript/src/jest/index.d.ts +1 -1
  57. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  58. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  59. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  60. package/lib/typescript/src/types/MarkdownTextProps.d.ts +46 -3
  61. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  62. package/lib/typescript/src/types/events.d.ts +36 -0
  63. package/lib/typescript/src/types/events.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/EnrichedMarkdownNativeComponent.ts +70 -1
  66. package/src/EnrichedMarkdownTextNativeComponent.ts +70 -1
  67. package/src/index.tsx +3 -0
  68. package/src/jest/index.tsx +22 -1
  69. package/src/native/EnrichedMarkdownText.tsx +87 -9
  70. package/src/types/MarkdownTextProps.ts +53 -2
  71. 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
@@ -134,11 +134,31 @@ void ENRMEnumerateLinkChipFragments(NSLayoutManager *layoutManager, NSTextContai
134
134
 
135
135
  CGRect textRect = [layoutManager boundingRectForGlyphRange:intersect
136
136
  inTextContainer:textContainer];
137
- CGFloat left = CGRectGetMinX(textRect);
138
- CGFloat right = CGRectGetMaxX(textRect);
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
- // The trailing spacer's kern is its advance; don't rely on the
141
- // glyph bounds including it.
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 enumerateAttribute:ENRMLinkChipAttributeName
181
- inRange:NSMakeRange(0, storage.length)
182
- options:0
183
- usingBlock:^(ENRMLinkChip *chip, NSRange range, BOOL *stop) {
184
- if (!chip.isVisible || NSIntersectionRange(range, charRange).length == 0)
185
- return;
186
-
187
- ENRMEnumerateLinkChipFragments(
188
- layoutManager, textContainer, range, chip, ^(CGRect rect, BOOL isFirst, BOOL isLast) {
189
- ENRMDrawChipFragment(CGRectOffset(rect, origin.x, origin.y), chip.backgroundColor,
190
- chip.borderColor, chip.borderWidth, chip.borderRadius, isFirst, isLast,
191
- YES);
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
- layoutManager, textContainer, range, chip, ^(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
- });
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
@@ -1,7 +1,7 @@
1
1
  #import "ContextMenuUtils.h"
2
+ #import "ENRMLinkChip.h"
2
3
  #import "ENRMMenuAction.h"
3
4
  #import "EditMenuUtils.h"
4
- #import "ENRMLinkChip.h"
5
5
  #import "PasteboardUtils.h"
6
6
  #import "StyleConfig.h"
7
7
  #include <TargetConditionals.h>
@@ -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,