react-native-advanced-text 0.1.43 → 0.2.0

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.
@@ -13,8 +13,25 @@ Pod::Spec.new do |s|
13
13
  s.platforms = { :ios => min_ios_version_supported }
14
14
  s.source = { :git => "https://github.com/AminAllahham/react-native-advanced-text.git", :tag => "#{s.version}" }
15
15
 
16
- s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
16
+ # `common/cpp` holds the self-measuring Fabric ShadowNode. Its
17
+ # ShadowNodes.h intentionally shadows the codegen output, so `common/cpp`
18
+ # must come first on the header search path (see below).
19
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "common/cpp/**/*.{h,cpp}"
17
20
  s.private_header_files = "ios/**/*.h"
18
21
 
19
22
  install_modules_dependencies(s)
23
+
24
+ # `install_modules_dependencies` sets `s.pod_target_xcconfig`; extend it
25
+ # (rather than replace it) so our headers win over the generated ones and
26
+ # so `React-FabricComponents` headers (TextLayoutManager) resolve.
27
+ xcconfig = s.attributes_hash["pod_target_xcconfig"] || {}
28
+ existing_header_paths = xcconfig["HEADER_SEARCH_PATHS"] || "$(inherited)"
29
+ xcconfig["HEADER_SEARCH_PATHS"] =
30
+ "\"$(PODS_TARGET_SRCROOT)/common/cpp\" #{existing_header_paths}"
31
+ xcconfig["CLANG_CXX_LANGUAGE_STANDARD"] ||= "c++20"
32
+ s.pod_target_xcconfig = xcconfig
33
+
34
+ # Needed for <react/renderer/textlayoutmanager/TextLayoutManager.h>, which is
35
+ # not pulled in by `install_modules_dependencies`.
36
+ s.dependency "React-FabricComponents"
20
37
  end
package/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  npm install react-native-advanced-text
10
10
  ```
11
11
 
12
+ > Requires the New Architecture (Fabric). React Native 0.80+ recommended.
12
13
 
13
14
  ## Usage
14
15
 
@@ -18,7 +19,7 @@ import { AdvancedText } from "react-native-advanced-text";
18
19
 
19
20
  <AdvancedText
20
21
  text={'This is an example of AdvancedText component. Tap on any word to see the event in action.'}
21
- style={[styles.AdvancedText, { minHeight }]}
22
+ style={styles.AdvancedText}
22
23
  onWordPress={(event) => {
23
24
  console.log({event})
24
25
  }}
@@ -42,9 +43,39 @@ import { AdvancedText } from "react-native-advanced-text";
42
43
  color={'#FFFFFF'}
43
44
  fontWeight="normal"
44
45
  fontFamily={'monospace'}
46
+ letterSpacing={1}
45
47
  />
46
48
  ```
47
49
 
50
+ ## Sizing
51
+
52
+ `AdvancedText` measures its own height, like React Native's built‑in `<Text>`.
53
+ Give it a width (via `style`, `flex`, or a constraining parent) and it derives
54
+ its height from the text content and that width — **no explicit `height` is
55
+ required**.
56
+
57
+ Under the hood the Fabric ShadowNode implements `measureContent()` using React
58
+ Native's own `TextLayoutManager` (the same engine `<Text>` uses), so wrapping,
59
+ font metrics, `letterSpacing` and `lineHeight` are accounted for on both iOS and
60
+ Android without any JS‑side text measurement.
61
+
62
+ ```jsx
63
+ // Before: an explicit height had to be estimated and passed in.
64
+ <AdvancedText text={text} style={{ height: estimatedHeight }} />
65
+
66
+ // Now:
67
+ <AdvancedText text={text} style={{ width: '100%' }} />
68
+ ```
69
+
70
+ ### Props that affect measurement
71
+
72
+ | Prop | Notes |
73
+ | --------------- | ------------------------------------------------------------------- |
74
+ | `fontSize` | points (defaults to 16) |
75
+ | `fontFamily` | |
76
+ | `fontWeight` | `"bold"` or `"italic"` |
77
+ | `letterSpacing` | points; applied to both rendering and measurement |
78
+ | `lineHeight` | multiple of the font size (unchanged behavior) |
48
79
 
49
80
  ## Contributing
50
81
 
@@ -0,0 +1,51 @@
1
+ # Custom CMake build for react-native-advanced-text.
2
+ #
3
+ # This replaces the auto-generated
4
+ # build/generated/source/codegen/jni/CMakeLists.txt
5
+ # (referenced from react-native.config.js via `cmakeListsPath`) so that the
6
+ # Fabric component is backed by our self-measuring ShadowNode
7
+ # (common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.{h,cpp})
8
+ # instead of the plain, non-measuring one that codegen emits.
9
+ #
10
+ # It still produces the target name the app autolinking expects:
11
+ # react_codegen_AdvancedTextViewSpec
12
+ #
13
+ # The key detail is include order: `common/cpp` is listed BEFORE the codegen
14
+ # output dir, so `#include <react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h>`
15
+ # resolves to our version, while Props.h / EventEmitters.h / States.h still
16
+ # resolve to the generated ones.
17
+
18
+ cmake_minimum_required(VERSION 3.13)
19
+ set(CMAKE_VERBOSE_MAKEFILE ON)
20
+
21
+ set(CODEGEN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/build/generated/source/codegen/jni")
22
+ set(COMMON_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../common/cpp")
23
+
24
+ file(GLOB codegen_SRCS CONFIGURE_DEPENDS
25
+ "${CODEGEN_DIR}/*.cpp"
26
+ "${CODEGEN_DIR}/react/renderer/components/AdvancedTextViewSpec/*.cpp")
27
+
28
+ file(GLOB custom_SRCS CONFIGURE_DEPENDS
29
+ "${COMMON_DIR}/react/renderer/components/AdvancedTextViewSpec/*.cpp")
30
+
31
+ add_library(
32
+ react_codegen_AdvancedTextViewSpec
33
+ OBJECT
34
+ ${codegen_SRCS}
35
+ ${custom_SRCS}
36
+ )
37
+
38
+ # `common/cpp` MUST come first so our ShadowNodes.h shadows the generated one.
39
+ target_include_directories(react_codegen_AdvancedTextViewSpec PUBLIC
40
+ "${COMMON_DIR}"
41
+ "${CODEGEN_DIR}"
42
+ "${CODEGEN_DIR}/react/renderer/components/AdvancedTextViewSpec")
43
+
44
+ target_link_libraries(
45
+ react_codegen_AdvancedTextViewSpec
46
+ fbjni
47
+ jsi
48
+ reactnative
49
+ )
50
+
51
+ target_compile_reactnative_options(react_codegen_AdvancedTextViewSpec PRIVATE)
@@ -41,9 +41,12 @@ class AdvancedTextView : TextView {
41
41
  private var textAlign: String = "left"
42
42
  private var fontFamily: String = "sans-serif"
43
43
  private var lineHeightMultiplier: Float = 1.0f
44
+ // Value from JS, in dp/points (0 = unset). Converted to em for TextView.
45
+ private var letterSpacingDp: Float = 0f
44
46
 
45
47
  private var indicatorWordIndex: Int = -1
46
48
  private var indicatorColor: String = ""
49
+ private var indicatorMode: String = "Highlight"
47
50
 
48
51
  private var wordPositions: List<WordPosition> = emptyList()
49
52
 
@@ -55,7 +58,10 @@ class AdvancedTextView : TextView {
55
58
  Log.d(TAG, "AdvancedTextView initialized")
56
59
 
57
60
  textSize = 16f
58
- setPadding(16, 16, 16, 16)
61
+ // Zero intrinsic padding so the rendered text matches the size reported
62
+ // by the Fabric ShadowNode's measureContent(). Consumers add padding via
63
+ // React Native `style` instead, exactly like <Text>.
64
+ setPadding(0, 0, 0, 0)
59
65
  setTextIsSelectable(true)
60
66
 
61
67
  movementMethod = SmartMovementMethod
@@ -147,6 +153,12 @@ class AdvancedTextView : TextView {
147
153
  updateTextWithHighlights()
148
154
  }
149
155
 
156
+ fun setAdvancedLetterSpacing(spacingDp: Float) {
157
+ if (letterSpacingDp == spacingDp) return
158
+ letterSpacingDp = spacingDp
159
+ updateTextWithHighlights()
160
+ }
161
+
150
162
 
151
163
  fun setAdvancedFontFamily(family: String) {
152
164
  if (fontFamily == family) return
@@ -162,12 +174,27 @@ class AdvancedTextView : TextView {
162
174
  fun setIndicatorWordIndex(index: Int) {
163
175
  if (indicatorWordIndex == index) return
164
176
  indicatorWordIndex = index
165
- invalidate()
177
+ if (indicatorMode == "fill") {
178
+ updateTextWithHighlights()
179
+ } else {
180
+ invalidate()
181
+ }
166
182
  }
167
183
 
168
184
  fun setIndicatorColor(color: String) {
169
185
  if (indicatorColor == color) return
170
186
  indicatorColor = color
187
+ if (indicatorMode == "fill") {
188
+ updateTextWithHighlights()
189
+ } else {
190
+ invalidate()
191
+ }
192
+ }
193
+
194
+ fun setIndicatorMode(mode: String) {
195
+ if (indicatorMode == mode) return
196
+ indicatorMode = mode
197
+ updateTextWithHighlights()
171
198
  invalidate()
172
199
  }
173
200
 
@@ -214,11 +241,19 @@ class AdvancedTextView : TextView {
214
241
  val spannableString = SpannableString(currentText)
215
242
 
216
243
  wordPositions.forEach { wordPos ->
244
+ val wordColor = if (indicatorMode == "fill" &&
245
+ indicatorWordIndex >= 0 &&
246
+ wordPos.index <= indicatorWordIndex &&
247
+ indicatorColor.isNotEmpty()) {
248
+ parseColor(indicatorColor)
249
+ } else {
250
+ parseColor(textColor)
251
+ }
217
252
  spannableString.setSpan(
218
253
  WordClickableSpan(
219
254
  wordIndex = wordPos.index,
220
255
  word = wordPos.word,
221
- wordColor = parseColor(textColor)
256
+ wordColor = wordColor
222
257
  ),
223
258
  wordPos.start,
224
259
  wordPos.end,
@@ -244,6 +279,14 @@ class AdvancedTextView : TextView {
244
279
 
245
280
  setLineSpacing(0f, lineHeightMultiplier)
246
281
 
282
+ // Android's letterSpacing is in em; the prop is in dp/points. The ratio
283
+ // dp / fontSize is scale-invariant, so no density conversion is needed.
284
+ letterSpacing = if (letterSpacingDp != 0f && fontSize > 0f) {
285
+ letterSpacingDp / fontSize
286
+ } else {
287
+ 0f
288
+ }
289
+
247
290
 
248
291
  post {
249
292
  setText(spannableString, BufferType.SPANNABLE)
@@ -299,8 +342,8 @@ class AdvancedTextView : TextView {
299
342
  }
300
343
  }
301
344
 
302
- // Draw indicator for indicatorWordIndex
303
- if (indicatorWordIndex >= 0 && indicatorColor.isNotEmpty()) {
345
+ // Draw indicator background only in Highlight mode
346
+ if (indicatorMode != "fill" && indicatorWordIndex >= 0 && indicatorColor.isNotEmpty()) {
304
347
  val wordPos = wordPositions.find { it.index == indicatorWordIndex }
305
348
  if (wordPos != null) {
306
349
  val start = wordPos.start
@@ -136,6 +136,14 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
136
136
  }
137
137
  }
138
138
 
139
+ @ReactProp(name = "indicatorMode")
140
+ fun setIndicatorMode(view: AdvancedTextView?, indicatorMode: String?) {
141
+ android.util.Log.d(NAME, "setIndicatorMode called with: $indicatorMode")
142
+ if (indicatorMode != null) {
143
+ view?.setIndicatorMode(indicatorMode)
144
+ }
145
+ }
146
+
139
147
  @ReactProp(name = "lineHeight")
140
148
  fun setLineHeight(view: AdvancedTextView?, lineHeight: Float) {
141
149
  android.util.Log.d(NAME, "setLineHeight called with: $lineHeight")
@@ -144,6 +152,12 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
144
152
  }
145
153
  }
146
154
 
155
+ @ReactProp(name = "letterSpacing")
156
+ fun setLetterSpacing(view: AdvancedTextView?, letterSpacing: Float) {
157
+ android.util.Log.d(NAME, "setLetterSpacing called with: $letterSpacing")
158
+ view?.setAdvancedLetterSpacing(letterSpacing)
159
+ }
160
+
147
161
  override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
148
162
  return mapOf(
149
163
  "onWordPress" to mapOf("registrationName" to "onWordPress"),
@@ -0,0 +1,120 @@
1
+ /*
2
+ * Implementation of the self-measuring <AdvancedTextView> ShadowNode.
3
+ * See ShadowNodes.h for why this file shadows the codegen output.
4
+ *
5
+ * NOTE: this file deliberately does NOT define `AdvancedTextViewComponentName`.
6
+ * That symbol stays owned by the codegen-generated `ShadowNodes.cpp`, which is
7
+ * still compiled (via the podspec on iOS and via CMakeLists.txt on Android).
8
+ */
9
+
10
+ #include <react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h>
11
+
12
+ #include <algorithm>
13
+ #include <cctype>
14
+ #include <string>
15
+
16
+ #include <react/renderer/attributedstring/AttributedStringBox.h>
17
+ #include <react/renderer/attributedstring/ParagraphAttributes.h>
18
+ #include <react/renderer/attributedstring/TextAttributes.h>
19
+ #include <react/renderer/attributedstring/primitives.h>
20
+ #include <react/renderer/mounting/ShadowView.h>
21
+ #include <react/renderer/textlayoutmanager/TextLayoutContext.h>
22
+ #include <react/renderer/textlayoutmanager/TextLayoutManager.h>
23
+
24
+ namespace facebook::react {
25
+
26
+ namespace {
27
+
28
+ std::string toLowerAscii(std::string value) {
29
+ std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
30
+ return static_cast<char>(std::tolower(c));
31
+ });
32
+ return value;
33
+ }
34
+
35
+ } // namespace
36
+
37
+ AdvancedTextViewShadowNode::AdvancedTextViewShadowNode(
38
+ const ShadowNode& sourceShadowNode,
39
+ const ShadowNodeFragment& fragment)
40
+ : ConcreteViewShadowNode(sourceShadowNode, fragment),
41
+ textLayoutManager_(
42
+ static_cast<const AdvancedTextViewShadowNode&>(sourceShadowNode)
43
+ .textLayoutManager_) {}
44
+
45
+ AttributedString AdvancedTextViewShadowNode::getAttributedString() const {
46
+ const auto& props = getConcreteProps();
47
+
48
+ auto textAttributes = TextAttributes::defaultTextAttributes();
49
+
50
+ // Font size: matches the 16pt fallback used by the native views.
51
+ textAttributes.fontSize =
52
+ props.fontSize > 0 ? static_cast<Float>(props.fontSize) : 16.0;
53
+
54
+ if (!props.fontFamily.empty()) {
55
+ textAttributes.fontFamily = props.fontFamily;
56
+ }
57
+
58
+ // The native views treat `fontWeight` as either a weight ("bold") or a
59
+ // style ("italic"); mirror that here so wrapping metrics line up.
60
+ const auto weight = toLowerAscii(props.fontWeight);
61
+ if (weight == "bold") {
62
+ textAttributes.fontWeight = FontWeight::Bold;
63
+ } else if (weight == "italic") {
64
+ textAttributes.fontStyle = FontStyle::Italic;
65
+ }
66
+
67
+ if (props.letterSpacing != 0) {
68
+ textAttributes.letterSpacing = props.letterSpacing;
69
+ }
70
+
71
+ if (props.lineHeight > 0) {
72
+ // The component treats `lineHeight` as a multiple of the font size
73
+ // (see AdvancedTextView.mm / AdvancedTextView.kt). `TextAttributes`
74
+ // expects an absolute value; approximate the platform default single
75
+ // line height as 1.2 * fontSize.
76
+ textAttributes.lineHeight =
77
+ props.lineHeight * textAttributes.fontSize * 1.2;
78
+ }
79
+
80
+ auto attributedString = AttributedString{};
81
+ attributedString.appendFragment(AttributedString::Fragment{
82
+ .string = props.text,
83
+ .textAttributes = textAttributes,
84
+ .parentShadowView = ShadowView(*this),
85
+ });
86
+ return attributedString;
87
+ }
88
+
89
+ Size AdvancedTextViewShadowNode::measureContent(
90
+ const LayoutContext& layoutContext,
91
+ const LayoutConstraints& layoutConstraints) const {
92
+ const auto& props = getConcreteProps();
93
+
94
+ if (props.text.empty()) {
95
+ return layoutConstraints.clamp({0, 0});
96
+ }
97
+
98
+ if (!textLayoutManager_) {
99
+ textLayoutManager_ =
100
+ std::make_shared<const TextLayoutManager>(getContextContainer());
101
+ }
102
+
103
+ auto paragraphAttributes = ParagraphAttributes{};
104
+ paragraphAttributes.maximumNumberOfLines = 0; // unlimited -> natural wrapping
105
+ paragraphAttributes.adjustsFontSizeToFit = false;
106
+
107
+ auto textLayoutContext = TextLayoutContext{};
108
+ textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
109
+ textLayoutContext.surfaceId = getSurfaceId();
110
+
111
+ auto measurement = textLayoutManager_->measure(
112
+ AttributedStringBox{getAttributedString()},
113
+ paragraphAttributes,
114
+ textLayoutContext,
115
+ layoutConstraints);
116
+
117
+ return layoutConstraints.clamp(measurement.size);
118
+ }
119
+
120
+ } // namespace facebook::react
@@ -0,0 +1,95 @@
1
+ /*
2
+ * Custom, self-measuring ShadowNode for <AdvancedTextView>.
3
+ *
4
+ * This file intentionally *shadows* the file generated by react-native-codegen
5
+ * at:
6
+ * build/generated/source/codegen/jni/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h (Android)
7
+ * <codegen>/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h (iOS)
8
+ *
9
+ * The generated version defines `AdvancedTextViewShadowNode` as a plain
10
+ * `ConcreteViewShadowNode` alias, which has no Yoga measure function. That is
11
+ * why <AdvancedText> renders nothing unless an explicit `height` is provided.
12
+ *
13
+ * This version replaces the alias with a real class that opts into Yoga
14
+ * measurement (`LeafYogaNode | MeasurableYogaNode`) and implements
15
+ * `measureContent()` using React Native's own `TextLayoutManager` -- the exact
16
+ * engine <Text> uses -- so the component derives its height from its text and
17
+ * the available width, just like <Text>.
18
+ *
19
+ * The generated `ComponentDescriptors.h` (`using AdvancedTextViewComponentDescriptor
20
+ * = ConcreteComponentDescriptor<AdvancedTextViewShadowNode>`) therefore wraps
21
+ * this class automatically; no other wiring change is required on either
22
+ * platform. The `AdvancedTextViewComponentName` symbol is still defined by the
23
+ * generated `ShadowNodes.cpp`.
24
+ *
25
+ * For this shadowing to take effect, `common/cpp` must appear on the include
26
+ * path *before* the codegen output directory:
27
+ * - Android: handled by `android/CMakeLists.txt` (see `target_include_directories`)
28
+ * - iOS: handled by `AdvancedText.podspec` (HEADER_SEARCH_PATHS)
29
+ */
30
+
31
+ #pragma once
32
+
33
+ #include <memory>
34
+
35
+ #include <jsi/jsi.h>
36
+ #include <react/renderer/attributedstring/AttributedString.h>
37
+ #include <react/renderer/components/AdvancedTextViewSpec/EventEmitters.h>
38
+ #include <react/renderer/components/AdvancedTextViewSpec/Props.h>
39
+ #include <react/renderer/components/AdvancedTextViewSpec/States.h>
40
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
41
+ #include <react/renderer/core/LayoutConstraints.h>
42
+ #include <react/renderer/core/LayoutContext.h>
43
+ #include <react/renderer/core/ShadowNode.h>
44
+ #include <react/renderer/core/ShadowNodeFragment.h>
45
+
46
+ namespace facebook::react {
47
+
48
+ class TextLayoutManager;
49
+
50
+ // Defined by the codegen-generated ShadowNodes.cpp.
51
+ JSI_EXPORT extern const char AdvancedTextViewComponentName[];
52
+
53
+ /*
54
+ * `ShadowNode` for <AdvancedTextView> component.
55
+ */
56
+ class AdvancedTextViewShadowNode final : public ConcreteViewShadowNode<
57
+ AdvancedTextViewComponentName,
58
+ AdvancedTextViewProps,
59
+ AdvancedTextViewEventEmitter,
60
+ AdvancedTextViewState> {
61
+ public:
62
+ // "create" constructor: (fragment, family, traits)
63
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
64
+
65
+ // "clone" constructor: carry the resolved TextLayoutManager across clones.
66
+ AdvancedTextViewShadowNode(
67
+ const ShadowNode& sourceShadowNode,
68
+ const ShadowNodeFragment& fragment);
69
+
70
+ static ShadowNodeTraits BaseTraits() {
71
+ auto traits = ConcreteViewShadowNode::BaseTraits();
72
+ // Opt into Yoga measurement. `MeasurableYogaNode` requires `LeafYogaNode`.
73
+ traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
74
+ traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
75
+ return traits;
76
+ }
77
+
78
+ #pragma mark - LayoutableShadowNode
79
+
80
+ Size measureContent(
81
+ const LayoutContext& layoutContext,
82
+ const LayoutConstraints& layoutConstraints) const override;
83
+
84
+ private:
85
+ /*
86
+ * Builds a single-fragment `AttributedString` from the component props
87
+ * (text + font attributes), mirroring how the native views render it.
88
+ */
89
+ AttributedString getAttributedString() const;
90
+
91
+ // Lazily created on first measure, then reused / copied across clones.
92
+ mutable std::shared_ptr<const TextLayoutManager> textLayoutManager_;
93
+ };
94
+
95
+ } // namespace facebook::react
@@ -23,8 +23,10 @@ using namespace facebook::react;
23
23
  @property (nonatomic, strong) NSString *textAlign;
24
24
  @property (nonatomic, strong) NSString *fontFamily;
25
25
  @property (nonatomic, assign) CGFloat lineHeight;
26
+ @property (nonatomic, assign) CGFloat letterSpacing;
26
27
  @property (nonatomic, assign) NSInteger indicatorWordIndex;
27
28
  @property (nonatomic, strong) UIColor *indicatorColor;
29
+ @property (nonatomic, strong) NSString *indicatorMode;
28
30
 
29
31
  - (void)handleCustomMenuAction:(UIMenuItem *)sender;
30
32
  - (void)drawHighlightedBackgroundsInTextView:(UITextView *)textView;
@@ -98,8 +100,10 @@ using namespace facebook::react;
98
100
  _textAlign = @"left";
99
101
  _fontFamily = @"System";
100
102
  _lineHeight = 0.0;
103
+ _letterSpacing = 0.0;
101
104
  _indicatorWordIndex = -1;
102
105
  _indicatorColor = nil;
106
+ _indicatorMode = @"Highlight";
103
107
 
104
108
  [self setupTextView];
105
109
  [self setupGestureRecognizers];
@@ -122,9 +126,17 @@ using namespace facebook::react;
122
126
  _textView.parentView = self;
123
127
  _textView.editable = NO;
124
128
  _textView.selectable = YES;
125
- _textView.scrollEnabled = YES;
129
+ // The Fabric ShadowNode now measures the intrinsic content height, so the
130
+ // native view is always laid out tall enough to fit its text. Disable
131
+ // scrolling so the component behaves like <Text> (content grows / clips
132
+ // rather than scrolls).
133
+ _textView.scrollEnabled = NO;
126
134
  _textView.backgroundColor = [UIColor clearColor];
127
- _textView.textContainerInset = UIEdgeInsetsMake(8, 8, 8, 8);
135
+ // Zero all intrinsic padding so the rendered text box matches the size
136
+ // reported by the ShadowNode's measureContent(). Consumers add padding
137
+ // via React Native `style` instead, exactly like <Text>.
138
+ _textView.textContainerInset = UIEdgeInsetsZero;
139
+ _textView.textContainer.lineFragmentPadding = 0;
128
140
  _textView.font = [UIFont systemFontOfSize:16];
129
141
  _textView.textColor = [UIColor labelColor];
130
142
  _textView.delegate = self;
@@ -234,10 +246,26 @@ using namespace facebook::react;
234
246
  styleChanged = YES;
235
247
  }
236
248
 
249
+ if (oldViewProps.letterSpacing != newViewProps.letterSpacing) {
250
+ NSLog(@"[AdvancedTextView] Updating letterSpacing to: %f", newViewProps.letterSpacing);
251
+ _letterSpacing = static_cast<CGFloat>(newViewProps.letterSpacing);
252
+ styleChanged = YES;
253
+ }
254
+
255
+ BOOL indicatorChanged = NO;
256
+
257
+ if (oldViewProps.indicatorMode != newViewProps.indicatorMode) {
258
+ NSLog(@"[AdvancedTextView] Updating indicatorMode to: %s", newViewProps.indicatorMode.c_str());
259
+ _indicatorMode = newViewProps.indicatorMode.empty()
260
+ ? @"Highlight"
261
+ : [NSString stringWithUTF8String:newViewProps.indicatorMode.c_str()];
262
+ indicatorChanged = YES;
263
+ }
264
+
237
265
  if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
238
266
  NSLog(@"[AdvancedTextView] Updating indicatorWordIndex to: %d", newViewProps.indicatorWordIndex);
239
267
  _indicatorWordIndex = static_cast<NSInteger>(newViewProps.indicatorWordIndex);
240
- [_textView setNeedsDisplay];
268
+ indicatorChanged = YES;
241
269
  }
242
270
 
243
271
  if (oldViewProps.indicatorColor != newViewProps.indicatorColor) {
@@ -248,7 +276,15 @@ using namespace facebook::react;
248
276
  } else {
249
277
  _indicatorColor = nil;
250
278
  }
251
- [_textView setNeedsDisplay];
279
+ indicatorChanged = YES;
280
+ }
281
+
282
+ if (indicatorChanged) {
283
+ if ([_indicatorMode isEqualToString:@"fill"]) {
284
+ [self updateTextAppearance]; // rebuilds attributed text with fill colors; calls setNeedsDisplay
285
+ } else {
286
+ [_textView setNeedsDisplay];
287
+ }
252
288
  }
253
289
 
254
290
  if (textChanged) {
@@ -446,12 +482,30 @@ using namespace facebook::react;
446
482
  value:font
447
483
  range:NSMakeRange(0, attributedString.length)];
448
484
 
485
+ if (_letterSpacing != 0.0) {
486
+ [attributedString addAttribute:NSKernAttributeName
487
+ value:@(_letterSpacing)
488
+ range:NSMakeRange(0, attributedString.length)];
489
+ }
490
+
449
491
 
450
492
  UIColor *color = _textColor ?: [UIColor labelColor];
451
493
  [attributedString addAttribute:NSForegroundColorAttributeName
452
494
  value:color
453
495
  range:NSMakeRange(0, attributedString.length)];
454
496
 
497
+ if ([_indicatorMode isEqualToString:@"fill"] && _indicatorWordIndex >= 0 && _indicatorColor) {
498
+ for (NSDictionary *wordInfo in _wordRanges) {
499
+ NSInteger idx = [wordInfo[@"index"] integerValue];
500
+ if (idx <= _indicatorWordIndex) {
501
+ NSRange wordRange = [wordInfo[@"range"] rangeValue];
502
+ [attributedString addAttribute:NSForegroundColorAttributeName
503
+ value:_indicatorColor
504
+ range:wordRange];
505
+ }
506
+ }
507
+ }
508
+
455
509
  _textView.attributedText = attributedString;
456
510
  [_textView setNeedsDisplay];
457
511
 
@@ -513,7 +567,7 @@ using namespace facebook::react;
513
567
  }];
514
568
  }
515
569
 
516
- if (_indicatorWordIndex >= 0 && _indicatorColor) {
570
+ if (![_indicatorMode isEqualToString:@"fill"] && _indicatorWordIndex >= 0 && _indicatorColor) {
517
571
  for (NSDictionary *wordInfo in _wordRanges) {
518
572
  NSNumber *index = wordInfo[@"index"];
519
573
  if ([index integerValue] != _indicatorWordIndex) {
@@ -22,8 +22,10 @@ export interface NativeProps extends ViewProps {
22
22
  textAlign?: string;
23
23
  fontFamily?: string;
24
24
  lineHeight?: Float;
25
+ letterSpacing?: Float;
25
26
  indicatorWordIndex?: Int32;
26
27
  indicatorColor?: string;
28
+ indicatorMode?: string;
27
29
  }
28
30
 
29
31
  export default codegenNativeComponent<NativeProps>('AdvancedTextView');
@@ -23,8 +23,10 @@ export interface NativeProps extends ViewProps {
23
23
  textAlign?: string;
24
24
  fontFamily?: string;
25
25
  lineHeight?: Float;
26
+ letterSpacing?: Float;
26
27
  indicatorWordIndex?: Int32;
27
28
  indicatorColor?: string;
29
+ indicatorMode?: string;
28
30
  }
29
31
  declare const _default: import("react-native/types_generated/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeProps>;
30
32
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IACjE,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;;AAED,wBAAuE"}
1
+ {"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IACjE,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;;AAED,wBAAuE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-advanced-text",
3
- "version": "0.1.43",
3
+ "version": "0.2.0",
4
4
  "description": " Advanced text component for React Native with custom select options.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -18,6 +18,7 @@
18
18
  "android",
19
19
  "ios",
20
20
  "cpp",
21
+ "common",
21
22
  "*.podspec",
22
23
  "react-native.config.js",
23
24
  "!ios/build",
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Autolinking config for react-native-advanced-text.
3
+ *
4
+ * On Android we ship a custom C++ ShadowNode (see android/CMakeLists.txt and
5
+ * common/cpp/...) so the Fabric component measures its own height from its text
6
+ * and the available width. Pointing `cmakeListsPath` at our own CMakeLists.txt
7
+ * makes the app build that instead of the plain codegen output.
8
+ *
9
+ * `libraryName` (AdvancedTextViewSpec) and `componentDescriptors`
10
+ * (AdvancedTextViewComponentDescriptor) are still auto-derived by the CLI from
11
+ * package.json `codegenConfig` and the JS spec, so they are intentionally not
12
+ * listed here.
13
+ */
14
+ module.exports = {
15
+ dependency: {
16
+ platforms: {
17
+ android: {
18
+ cmakeListsPath: 'CMakeLists.txt',
19
+ },
20
+ },
21
+ },
22
+ };
@@ -22,8 +22,10 @@ export interface NativeProps extends ViewProps {
22
22
  textAlign?: string;
23
23
  fontFamily?: string;
24
24
  lineHeight?: Float;
25
+ letterSpacing?: Float;
25
26
  indicatorWordIndex?: Int32;
26
27
  indicatorColor?: string;
28
+ indicatorMode?: string;
27
29
  }
28
30
 
29
31
  export default codegenNativeComponent<NativeProps>('AdvancedTextView');