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
|
@@ -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.
|
|
@@ -480,6 +528,16 @@ export interface NativeProps extends ViewProps {
|
|
|
480
528
|
* @default true
|
|
481
529
|
*/
|
|
482
530
|
enableLinkPreview?: CodegenTypes.WithDefault<boolean, true>;
|
|
531
|
+
/**
|
|
532
|
+
* When `true`, long-pressing a link selects its label and shows the
|
|
533
|
+
* selection edit menu instead of the system link preview (iOS only).
|
|
534
|
+
* Takes precedence over `enableLinkPreview` and `onLinkLongPress`.
|
|
535
|
+
*
|
|
536
|
+
* Android: No-op.
|
|
537
|
+
*
|
|
538
|
+
* @default false
|
|
539
|
+
*/
|
|
540
|
+
linkLongPressSelects?: CodegenTypes.WithDefault<boolean, false>;
|
|
483
541
|
/**
|
|
484
542
|
* - iOS: Controls text selection and link previews on long press.
|
|
485
543
|
* - Android: Controls text selection.
|
|
@@ -594,6 +652,17 @@ export interface NativeProps extends ViewProps {
|
|
|
594
652
|
writingDirection?: CodegenTypes.WithDefault<string, 'first-strong'>;
|
|
595
653
|
}
|
|
596
654
|
|
|
655
|
+
type ComponentType = HostComponent<NativeProps>;
|
|
656
|
+
|
|
657
|
+
interface NativeCommands {
|
|
658
|
+
/** Clears the text selection (and any active block) and fires `onSelectionChange` with `active: false`. */
|
|
659
|
+
clearSelection: (viewRef: React.ElementRef<ComponentType>) => void;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
663
|
+
supportedCommands: ['clearSelection'],
|
|
664
|
+
});
|
|
665
|
+
|
|
597
666
|
export default codegenNativeComponent<NativeProps>('EnrichedMarkdown', {
|
|
598
667
|
interfaceOnly: true,
|
|
599
|
-
})
|
|
668
|
+
}) as HostComponent<NativeProps>;
|
|
@@ -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 {
|
|
@@ -275,6 +278,39 @@ export interface LinkLayoutEvent {
|
|
|
275
278
|
}[];
|
|
276
279
|
}
|
|
277
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Current text selection, or a long-pressed table / math / code block while
|
|
283
|
+
* `contextMenuHidden` is on. `active: false` means the selection was cleared
|
|
284
|
+
* (every other field is then empty / zero). Rects are in dp / pt, relative to
|
|
285
|
+
* the native view's origin; rect objects are written inline because codegen
|
|
286
|
+
* only accepts inline object literals in event payloads.
|
|
287
|
+
*/
|
|
288
|
+
export interface SelectionChangeEvent {
|
|
289
|
+
active: boolean;
|
|
290
|
+
source: string;
|
|
291
|
+
text: string;
|
|
292
|
+
markdown: string;
|
|
293
|
+
whole: boolean;
|
|
294
|
+
rect: {
|
|
295
|
+
x: CodegenTypes.Double;
|
|
296
|
+
y: CodegenTypes.Double;
|
|
297
|
+
width: CodegenTypes.Double;
|
|
298
|
+
height: CodegenTypes.Double;
|
|
299
|
+
};
|
|
300
|
+
startRect: {
|
|
301
|
+
x: CodegenTypes.Double;
|
|
302
|
+
y: CodegenTypes.Double;
|
|
303
|
+
width: CodegenTypes.Double;
|
|
304
|
+
height: CodegenTypes.Double;
|
|
305
|
+
};
|
|
306
|
+
endRect: {
|
|
307
|
+
x: CodegenTypes.Double;
|
|
308
|
+
y: CodegenTypes.Double;
|
|
309
|
+
width: CodegenTypes.Double;
|
|
310
|
+
height: CodegenTypes.Double;
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
278
314
|
export interface TaskListItemPressEvent {
|
|
279
315
|
index: CodegenTypes.Int32;
|
|
280
316
|
checked: boolean;
|
|
@@ -442,6 +478,18 @@ export interface NativeProps extends ViewProps {
|
|
|
442
478
|
* Deduplicated: not re-fired when the payload is unchanged.
|
|
443
479
|
*/
|
|
444
480
|
onLinkLayout?: CodegenTypes.DirectEventHandler<LinkLayoutEvent>;
|
|
481
|
+
/**
|
|
482
|
+
* When true, the view reports selection changes (and, with
|
|
483
|
+
* `contextMenuHidden`, block long-presses) through `onSelectionChange`.
|
|
484
|
+
* Set by the JS wrapper only when `onSelectionChange` is provided.
|
|
485
|
+
* @default false
|
|
486
|
+
*/
|
|
487
|
+
emitSelectionChange?: CodegenTypes.WithDefault<boolean, false>;
|
|
488
|
+
/**
|
|
489
|
+
* Fired while `emitSelectionChange` is on: coalesced to one event per frame,
|
|
490
|
+
* deduplicated, and `active: false` once the selection is gone.
|
|
491
|
+
*/
|
|
492
|
+
onSelectionChange?: CodegenTypes.DirectEventHandler<SelectionChangeEvent>;
|
|
445
493
|
/**
|
|
446
494
|
* Callback fired when a task list checkbox is tapped.
|
|
447
495
|
* Receives the 0-based task index, current checked state, and the item's plain text.
|
|
@@ -481,6 +529,16 @@ export interface NativeProps extends ViewProps {
|
|
|
481
529
|
* @default true
|
|
482
530
|
*/
|
|
483
531
|
enableLinkPreview?: CodegenTypes.WithDefault<boolean, true>;
|
|
532
|
+
/**
|
|
533
|
+
* When `true`, long-pressing a link selects its label and shows the
|
|
534
|
+
* selection edit menu instead of the system link preview (iOS only).
|
|
535
|
+
* Takes precedence over `enableLinkPreview` and `onLinkLongPress`.
|
|
536
|
+
*
|
|
537
|
+
* Android: No-op.
|
|
538
|
+
*
|
|
539
|
+
* @default false
|
|
540
|
+
*/
|
|
541
|
+
linkLongPressSelects?: CodegenTypes.WithDefault<boolean, false>;
|
|
484
542
|
/**
|
|
485
543
|
* - iOS: Controls text selection and link previews on long press.
|
|
486
544
|
* - Android: Controls text selection.
|
|
@@ -594,6 +652,17 @@ export interface NativeProps extends ViewProps {
|
|
|
594
652
|
writingDirection?: CodegenTypes.WithDefault<string, 'first-strong'>;
|
|
595
653
|
}
|
|
596
654
|
|
|
655
|
+
type ComponentType = HostComponent<NativeProps>;
|
|
656
|
+
|
|
657
|
+
interface NativeCommands {
|
|
658
|
+
/** Clears the text selection (and any active block) and fires `onSelectionChange` with `active: false`. */
|
|
659
|
+
clearSelection: (viewRef: React.ElementRef<ComponentType>) => void;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
663
|
+
supportedCommands: ['clearSelection'],
|
|
664
|
+
});
|
|
665
|
+
|
|
597
666
|
export default codegenNativeComponent<NativeProps>('EnrichedMarkdownText', {
|
|
598
667
|
interfaceOnly: true,
|
|
599
|
-
})
|
|
668
|
+
}) as HostComponent<NativeProps>;
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as EnrichedMarkdownText } from './native/EnrichedMarkdownText';
|
|
2
2
|
export type {
|
|
3
3
|
EnrichedMarkdownTextProps,
|
|
4
|
+
EnrichedMarkdownTextInstance,
|
|
4
5
|
StreamingConfig,
|
|
5
6
|
MarkdownStyle,
|
|
6
7
|
Md4cFlags,
|
|
@@ -17,6 +18,8 @@ export type {
|
|
|
17
18
|
CopyPressEvent,
|
|
18
19
|
CopyEvent,
|
|
19
20
|
OnPasteEvent,
|
|
21
|
+
SelectionChangeEvent,
|
|
22
|
+
SelectionRect,
|
|
20
23
|
} from './types/events';
|
|
21
24
|
export type {
|
|
22
25
|
AccessibilityLabels,
|
package/src/jest/index.tsx
CHANGED
|
@@ -26,7 +26,10 @@ import type {
|
|
|
26
26
|
EnrichedMarkdownTextInputProps,
|
|
27
27
|
InsertMathOptions,
|
|
28
28
|
} from '../EnrichedMarkdownTextInput';
|
|
29
|
-
import type {
|
|
29
|
+
import type {
|
|
30
|
+
EnrichedMarkdownTextInstance,
|
|
31
|
+
EnrichedMarkdownTextProps,
|
|
32
|
+
} from '../native/EnrichedMarkdownText';
|
|
30
33
|
|
|
31
34
|
export {
|
|
32
35
|
DEFAULT_ACCESSIBILITY_LABELS,
|
|
@@ -165,6 +168,7 @@ export const EnrichedMarkdownTextInput = ({
|
|
|
165
168
|
};
|
|
166
169
|
|
|
167
170
|
export const EnrichedMarkdownText = ({
|
|
171
|
+
ref,
|
|
168
172
|
markdown,
|
|
169
173
|
containerStyle,
|
|
170
174
|
testID,
|
|
@@ -178,7 +182,9 @@ export const EnrichedMarkdownText = ({
|
|
|
178
182
|
onLinkPress: _onLinkPress,
|
|
179
183
|
onLinkLongPress: _onLinkLongPress,
|
|
180
184
|
onTaskListItemPress: _onTaskListItemPress,
|
|
185
|
+
onSelectionChange: _onSelectionChange,
|
|
181
186
|
enableLinkPreview: _enableLinkPreview,
|
|
187
|
+
linkLongPressSelects: _linkLongPressSelects,
|
|
182
188
|
selectable: _selectable,
|
|
183
189
|
md4cFlags: _md4cFlags,
|
|
184
190
|
allowFontScaling: _allowFontScaling,
|
|
@@ -198,6 +204,21 @@ export const EnrichedMarkdownText = ({
|
|
|
198
204
|
lineBreakStrategyIOS: _lineBreakStrategyIOS,
|
|
199
205
|
writingDirection: _writingDirection,
|
|
200
206
|
}: EnrichedMarkdownTextProps) => {
|
|
207
|
+
const instanceRef = useRef<EnrichedMarkdownTextInstance | null>(null);
|
|
208
|
+
if (instanceRef.current === null) {
|
|
209
|
+
instanceRef.current = {
|
|
210
|
+
// No native selection under Jest; the mock records the call.
|
|
211
|
+
clearSelection: spy(() => {}),
|
|
212
|
+
measure: spy((callback) => callback(0, 0, 0, 0, 0, 0)),
|
|
213
|
+
measureInWindow: spy((callback) => callback(0, 0, 0, 0)),
|
|
214
|
+
measureLayout: spy((_relativeToNativeNode, onSuccess) =>
|
|
215
|
+
onSuccess(0, 0, 0, 0)
|
|
216
|
+
),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
useImperativeHandle(ref, () => instanceRef.current!);
|
|
221
|
+
|
|
201
222
|
return (
|
|
202
223
|
<Text
|
|
203
224
|
style={containerStyle}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
useCallback,
|
|
4
|
+
useRef,
|
|
5
|
+
useEffect,
|
|
6
|
+
useState,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import EnrichedMarkdownTextNativeComponent, {
|
|
10
|
+
Commands as TextCommands,
|
|
11
|
+
} from '../EnrichedMarkdownTextNativeComponent';
|
|
3
12
|
import type {
|
|
4
13
|
LinkLayoutEvent,
|
|
5
14
|
MarkdownStyleInternal,
|
|
15
|
+
SelectionChangeEvent as NativeSelectionChangeEvent,
|
|
6
16
|
} from '../EnrichedMarkdownTextNativeComponent';
|
|
7
|
-
import EnrichedMarkdownNativeComponent
|
|
17
|
+
import EnrichedMarkdownNativeComponent, {
|
|
18
|
+
Commands as MarkdownCommands,
|
|
19
|
+
} from '../EnrichedMarkdownNativeComponent';
|
|
8
20
|
import { normalizeMarkdownStyle } from '../normalizeMarkdownStyle';
|
|
9
21
|
import { resolveAccessibilityLabels } from '../accessibilityLabelDefaults';
|
|
10
22
|
import {
|
|
@@ -12,10 +24,15 @@ import {
|
|
|
12
24
|
normalizeLegacyBooleanMenuItem,
|
|
13
25
|
} from '../normalizeMenuItem';
|
|
14
26
|
import { StyleSheet, View } from 'react-native';
|
|
15
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
HostInstance,
|
|
29
|
+
LayoutChangeEvent,
|
|
30
|
+
NativeSyntheticEvent,
|
|
31
|
+
} from 'react-native';
|
|
16
32
|
import type { MarkdownStyle, Md4cFlags } from '../types/MarkdownStyle';
|
|
17
33
|
import type {
|
|
18
34
|
EnrichedMarkdownTextProps,
|
|
35
|
+
EnrichedMarkdownTextInstance,
|
|
19
36
|
StreamingConfig,
|
|
20
37
|
ContextMenuItem,
|
|
21
38
|
SelectionMenuConfig,
|
|
@@ -30,11 +47,14 @@ import type {
|
|
|
30
47
|
CopyPressEvent,
|
|
31
48
|
CopyEvent,
|
|
32
49
|
OnContextMenuItemPressEvent,
|
|
50
|
+
SelectionChangeEvent,
|
|
51
|
+
SelectionRect,
|
|
33
52
|
} from '../types/events';
|
|
34
53
|
|
|
35
54
|
export type { MarkdownStyle, Md4cFlags };
|
|
36
55
|
export type {
|
|
37
56
|
EnrichedMarkdownTextProps,
|
|
57
|
+
EnrichedMarkdownTextInstance,
|
|
38
58
|
StreamingConfig,
|
|
39
59
|
ContextMenuItem,
|
|
40
60
|
SelectionMenuConfig,
|
|
@@ -48,6 +68,8 @@ export type {
|
|
|
48
68
|
TaskListItemPressEvent,
|
|
49
69
|
CopyPressEvent,
|
|
50
70
|
CopyEvent,
|
|
71
|
+
SelectionChangeEvent,
|
|
72
|
+
SelectionRect,
|
|
51
73
|
};
|
|
52
74
|
|
|
53
75
|
// Default English labels for the built-in selection menu actions. Defaults are
|
|
@@ -149,6 +171,7 @@ const defaultMd4cFlags: Md4cFlags = {
|
|
|
149
171
|
};
|
|
150
172
|
|
|
151
173
|
export const EnrichedMarkdownText = ({
|
|
174
|
+
ref,
|
|
152
175
|
markdown,
|
|
153
176
|
markdownStyle = {},
|
|
154
177
|
containerStyle,
|
|
@@ -158,7 +181,9 @@ export const EnrichedMarkdownText = ({
|
|
|
158
181
|
enableTaskListItemToggle = true,
|
|
159
182
|
onCopyPress,
|
|
160
183
|
onCopy,
|
|
184
|
+
onSelectionChange,
|
|
161
185
|
enableLinkPreview,
|
|
186
|
+
linkLongPressSelects = false,
|
|
162
187
|
selectable = true,
|
|
163
188
|
md4cFlags = defaultMd4cFlags,
|
|
164
189
|
allowFontScaling = true,
|
|
@@ -315,6 +340,54 @@ export const EnrichedMarkdownText = ({
|
|
|
315
340
|
setNativeOffset((prev) => (prev.x === x && prev.y === y ? prev : { x, y }));
|
|
316
341
|
}, []);
|
|
317
342
|
|
|
343
|
+
// Rects stay relative to the native view — the view the ref's
|
|
344
|
+
// `measureInWindow` measures — even when decorations wrap it.
|
|
345
|
+
const handleSelectionChange = useCallback(
|
|
346
|
+
// Codegen types `source` as a plain string; native only sends the four
|
|
347
|
+
// values of SelectionChangeEvent['source'].
|
|
348
|
+
(e: NativeSyntheticEvent<NativeSelectionChangeEvent>) => {
|
|
349
|
+
const { source, ...payload } = e.nativeEvent;
|
|
350
|
+
onSelectionChange?.({
|
|
351
|
+
...payload,
|
|
352
|
+
source: source as SelectionChangeEvent['source'],
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
[onSelectionChange]
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const nativeRef = useRef<HostInstance | null>(null);
|
|
359
|
+
const isGithubFlavor = flavor === 'github';
|
|
360
|
+
useImperativeHandle(
|
|
361
|
+
ref,
|
|
362
|
+
() => ({
|
|
363
|
+
clearSelection: () => {
|
|
364
|
+
const node = nativeRef.current;
|
|
365
|
+
if (node == null) return;
|
|
366
|
+
// Codegen's ViewRef resolves to `never` with RN 0.84's function-based
|
|
367
|
+
// HostComponent type — the cast is safe at runtime.
|
|
368
|
+
if (isGithubFlavor) {
|
|
369
|
+
MarkdownCommands.clearSelection(
|
|
370
|
+
node as Parameters<(typeof MarkdownCommands)['clearSelection']>[0]
|
|
371
|
+
);
|
|
372
|
+
} else {
|
|
373
|
+
TextCommands.clearSelection(
|
|
374
|
+
node as Parameters<(typeof TextCommands)['clearSelection']>[0]
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
measure: (callback) => nativeRef.current?.measure(callback),
|
|
379
|
+
measureInWindow: (callback) =>
|
|
380
|
+
nativeRef.current?.measureInWindow(callback),
|
|
381
|
+
measureLayout: (relativeToNativeNode, onSuccess, onFail) =>
|
|
382
|
+
nativeRef.current?.measureLayout(
|
|
383
|
+
relativeToNativeNode,
|
|
384
|
+
onSuccess,
|
|
385
|
+
onFail
|
|
386
|
+
),
|
|
387
|
+
}),
|
|
388
|
+
[isGithubFlavor]
|
|
389
|
+
);
|
|
390
|
+
|
|
318
391
|
const tableMode = streamingConfig?.tableMode ?? 'progressive';
|
|
319
392
|
const codeBlockMode = streamingConfig?.codeBlockMode ?? 'progressive';
|
|
320
393
|
const normalizedStreamingConfig = useMemo(
|
|
@@ -396,7 +469,10 @@ export const EnrichedMarkdownText = ({
|
|
|
396
469
|
enableTaskListItemToggle,
|
|
397
470
|
onCopyPress: handleCopyPress,
|
|
398
471
|
onCopyRequest: handleCopyRequest,
|
|
472
|
+
emitSelectionChange: onSelectionChange != null,
|
|
473
|
+
onSelectionChange: handleSelectionChange,
|
|
399
474
|
enableLinkPreview: onLinkLongPress == null && (enableLinkPreview ?? true),
|
|
475
|
+
linkLongPressSelects,
|
|
400
476
|
selectable,
|
|
401
477
|
md4cFlags: normalizedMd4cFlags,
|
|
402
478
|
allowFontScaling,
|
|
@@ -420,21 +496,23 @@ export const EnrichedMarkdownText = ({
|
|
|
420
496
|
...rest,
|
|
421
497
|
};
|
|
422
498
|
|
|
423
|
-
const NativeComponent =
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
: EnrichedMarkdownTextNativeComponent;
|
|
499
|
+
const NativeComponent = isGithubFlavor
|
|
500
|
+
? EnrichedMarkdownNativeComponent
|
|
501
|
+
: EnrichedMarkdownTextNativeComponent;
|
|
427
502
|
|
|
428
503
|
// Without decorations the native view is returned as-is (no wrapper, no
|
|
429
504
|
// layout events) so existing layouts are untouched.
|
|
430
505
|
if (!decorated) {
|
|
431
|
-
return
|
|
506
|
+
return (
|
|
507
|
+
<NativeComponent {...sharedProps} ref={nativeRef} onLayout={onLayout} />
|
|
508
|
+
);
|
|
432
509
|
}
|
|
433
510
|
|
|
434
511
|
return (
|
|
435
512
|
<View style={containerStyle} onLayout={onLayout}>
|
|
436
513
|
<NativeComponent
|
|
437
514
|
{...sharedProps}
|
|
515
|
+
ref={nativeRef}
|
|
438
516
|
style={undefined}
|
|
439
517
|
emitLinkLayout
|
|
440
518
|
onLinkLayout={handleLinkLayout}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
import type {
|
|
1
|
+
import type { ReactNode, RefObject } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
ColorValue,
|
|
4
|
+
HostInstance,
|
|
5
|
+
ViewProps,
|
|
6
|
+
ViewStyle,
|
|
7
|
+
TextStyle,
|
|
8
|
+
} from 'react-native';
|
|
3
9
|
import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle';
|
|
4
10
|
import type { AccessibilityLabels } from './AccessibilityLabels';
|
|
5
11
|
import type {
|
|
@@ -8,8 +14,22 @@ import type {
|
|
|
8
14
|
TaskListItemPressEvent,
|
|
9
15
|
CopyPressEvent,
|
|
10
16
|
CopyEvent,
|
|
17
|
+
SelectionChangeEvent,
|
|
11
18
|
} from './events';
|
|
12
19
|
|
|
20
|
+
/** Imperative handle of `EnrichedMarkdownText`, reached through its `ref`. */
|
|
21
|
+
export interface EnrichedMarkdownTextInstance {
|
|
22
|
+
/**
|
|
23
|
+
* Clears the text selection (and any long-pressed block) and fires
|
|
24
|
+
* `onSelectionChange` with `active: false`.
|
|
25
|
+
* @platform ios, android
|
|
26
|
+
*/
|
|
27
|
+
clearSelection: () => void;
|
|
28
|
+
measure: HostInstance['measure'];
|
|
29
|
+
measureInWindow: HostInstance['measureInWindow'];
|
|
30
|
+
measureLayout: HostInstance['measureLayout'];
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
/**
|
|
14
34
|
* A rect in dp, relative to the component's content box (inside any
|
|
15
35
|
* `containerStyle` padding).
|
|
@@ -169,6 +189,11 @@ export interface StreamingConfig {
|
|
|
169
189
|
}
|
|
170
190
|
|
|
171
191
|
export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
|
|
192
|
+
/**
|
|
193
|
+
* Imperative handle: `clearSelection()` plus the host `measure*` methods.
|
|
194
|
+
* @platform ios, android
|
|
195
|
+
*/
|
|
196
|
+
ref?: RefObject<EnrichedMarkdownTextInstance | null>;
|
|
172
197
|
/**
|
|
173
198
|
* Markdown content to render.
|
|
174
199
|
* @platform ios, android, web
|
|
@@ -269,6 +294,22 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
|
|
|
269
294
|
* @platform ios, android
|
|
270
295
|
*/
|
|
271
296
|
onCopy?: (event: CopyEvent) => void;
|
|
297
|
+
/**
|
|
298
|
+
* Reports the text selection so the app can draw its own menu (pair it with
|
|
299
|
+
* `contextMenuHidden`). Fires at most once per frame while the selection
|
|
300
|
+
* changes, and with `active: false` once it is gone. With
|
|
301
|
+
* `contextMenuHidden`, a long press on a table / math / code block fires it
|
|
302
|
+
* too (`source` is the block kind, `whole: true`), since those blocks have no
|
|
303
|
+
* text selection of their own. Only one part of a component holds a
|
|
304
|
+
* selection at a time; clearing the selection of *other* components is up to
|
|
305
|
+
* the app (`ref.current.clearSelection()`).
|
|
306
|
+
*
|
|
307
|
+
* Rects are relative to the view that `ref.current.measureInWindow`
|
|
308
|
+
* measures — add that origin to position UI on screen. There is no
|
|
309
|
+
* "dragging" flag: debounce before showing a menu.
|
|
310
|
+
* @platform ios, android
|
|
311
|
+
*/
|
|
312
|
+
onSelectionChange?: (event: SelectionChangeEvent) => void;
|
|
272
313
|
/**
|
|
273
314
|
* Controls whether the system link preview is shown on long press (iOS only).
|
|
274
315
|
*
|
|
@@ -282,6 +323,16 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
|
|
|
282
323
|
* @platform ios
|
|
283
324
|
*/
|
|
284
325
|
enableLinkPreview?: boolean;
|
|
326
|
+
/**
|
|
327
|
+
* When `true`, long-pressing a link selects its label and shows the
|
|
328
|
+
* selection edit menu, like a manual text selection, instead of the system
|
|
329
|
+
* link preview. Takes precedence over `enableLinkPreview` and
|
|
330
|
+
* `onLinkLongPress`.
|
|
331
|
+
*
|
|
332
|
+
* @default false
|
|
333
|
+
* @platform ios
|
|
334
|
+
*/
|
|
335
|
+
linkLongPressSelects?: boolean;
|
|
285
336
|
/**
|
|
286
337
|
* Controls text selection.
|
|
287
338
|
* - iOS: Controls text selection and link previews on long press.
|
package/src/types/events.ts
CHANGED
|
@@ -40,6 +40,44 @@ export interface CopyEvent {
|
|
|
40
40
|
whole: boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* A rect in dp, relative to the component's native view: add the origin from
|
|
45
|
+
* the component ref's `measureInWindow` to get window coordinates.
|
|
46
|
+
*/
|
|
47
|
+
export interface SelectionRect {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Payload of `onSelectionChange`: the current text selection, or a table /
|
|
56
|
+
* math / code block long-pressed while `contextMenuHidden` is on. Content
|
|
57
|
+
* fields carry what `onCopy` would receive for the same selection.
|
|
58
|
+
*/
|
|
59
|
+
export interface SelectionChangeEvent {
|
|
60
|
+
/** `false` once the selection is gone; every other field is then empty / zero. */
|
|
61
|
+
active: boolean;
|
|
62
|
+
/** `'selection'` for text; the block kind for a long-pressed block. */
|
|
63
|
+
source: CopyEvent['source'];
|
|
64
|
+
/** Rendered text of the selection, or the block's raw content. */
|
|
65
|
+
text: string;
|
|
66
|
+
/** Same as `CopyEvent.markdown` for this selection. */
|
|
67
|
+
markdown: string;
|
|
68
|
+
/** Same as `CopyEvent.whole`. Always `true` for blocks. */
|
|
69
|
+
whole: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Bounding box of the selection (full content width when it spans several
|
|
72
|
+
* lines); the block's frame for blocks.
|
|
73
|
+
*/
|
|
74
|
+
rect: SelectionRect;
|
|
75
|
+
/** Selected part of the first line (block: same as `rect`). */
|
|
76
|
+
startRect: SelectionRect;
|
|
77
|
+
/** Selected part of the last line (block: same as `rect`). */
|
|
78
|
+
endRect: SelectionRect;
|
|
79
|
+
}
|
|
80
|
+
|
|
43
81
|
/**
|
|
44
82
|
* Native-level context menu item config sent to the native component.
|
|
45
83
|
* Does not include the `onPress` callback — callbacks are managed on the JS side.
|