react-native-advanced-text 0.1.28 → 0.1.30

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/README.md CHANGED
@@ -1,37 +1,56 @@
1
- # react-native-advanced-text
2
-
3
- Advanced text component for React Native with custom select options.
4
-
5
- ## Installation
6
-
7
-
8
- ```sh
9
- npm install react-native-advanced-text
10
- ```
11
-
12
-
13
- ## Usage
14
-
15
-
16
- ```js
17
- import { AdvancedTextView } from "react-native-advanced-text";
18
-
19
- // ...
20
-
21
- <AdvancedTextView color="tomato" />
22
- ```
23
-
24
-
25
- ## Contributing
26
-
27
- - [Development workflow](CONTRIBUTING.md#development-workflow)
28
- - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
29
- - [Code of conduct](CODE_OF_CONDUCT.md)
30
-
31
- ## License
32
-
33
- MIT
34
-
35
- ---
36
-
37
- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
1
+ # react-native-advanced-text
2
+
3
+ react-native-advanced-text is a powerful cross-platform text component for React Native that enables word-level interaction, dynamic highlighting, and custom selection actions.
4
+
5
+ ## Installation
6
+
7
+
8
+ ```sh
9
+ npm install react-native-advanced-text
10
+ ```
11
+
12
+
13
+ ## Usage
14
+
15
+
16
+ ```js
17
+ import { AdvancedTextView } from "react-native-advanced-text";
18
+
19
+ <AdvancedText
20
+ text={'This is an example of AdvancedText component. Tap on any word to see the event in action.'}
21
+ style={[styles.AdvancedText, { minHeight }]}
22
+ indicatorWordIndex={2}
23
+ onWordPress={(event) => {
24
+ console.log({event})
25
+ }}
26
+ menuOptions={['Highlight', 'Copy', 'Translate']}
27
+ onSelection={(event) => {
28
+ console.log({event})
29
+ }}
30
+ highlightedWords={[
31
+ {
32
+ index: 4,
33
+ highlightColor: '#6baeffb5',
34
+ },
35
+ ]}
36
+ fontSize={24}
37
+ color={'#FFFFFF'}
38
+ fontWeight="normal"
39
+ fontFamily={'monospace'}
40
+ />
41
+ ```
42
+
43
+
44
+ ## Contributing
45
+
46
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
47
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
48
+ - [Code of conduct](CODE_OF_CONDUCT.md)
49
+
50
+ ## License
51
+
52
+ MIT
53
+
54
+ ---
55
+
56
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -2,10 +2,12 @@ package com.advancedtext
2
2
 
3
3
  import android.content.Context
4
4
  import android.graphics.Color
5
+ import android.graphics.Point
5
6
  import android.text.SpannableString
7
+ import android.text.Spannable
6
8
  import android.text.Spanned
7
9
  import android.text.TextPaint
8
- import android.text.method.LinkMovementMethod
10
+ import android.text.method.ArrowKeyMovementMethod
9
11
  import android.text.style.ClickableSpan
10
12
  import android.text.style.BackgroundColorSpan
11
13
  import android.text.style.ForegroundColorSpan
@@ -15,12 +17,14 @@ import android.view.ActionMode
15
17
  import android.view.Menu
16
18
  import android.view.MenuItem
17
19
  import android.view.View
20
+ import android.view.MotionEvent
18
21
  import android.widget.TextView
19
22
  import com.facebook.react.bridge.Arguments
20
23
  import com.facebook.react.bridge.ReactContext
21
24
  import com.facebook.react.uimanager.events.RCTEventEmitter
22
25
  import android.text.Selection
23
26
  import android.graphics.Typeface
27
+ import androidx.core.text.getSpans
24
28
 
25
29
  class AdvancedTextView : TextView {
26
30
 
@@ -49,9 +53,9 @@ class AdvancedTextView : TextView {
49
53
 
50
54
  textSize = 16f
51
55
  setPadding(16, 16, 16, 16)
52
- movementMethod = LinkMovementMethod.getInstance()
53
56
  setTextIsSelectable(true)
54
57
 
58
+ movementMethod = SmartMovementMethod
55
59
 
56
60
  customSelectionActionModeCallback = object : ActionMode.Callback {
57
61
  override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
@@ -94,8 +98,6 @@ class AdvancedTextView : TextView {
94
98
  }
95
99
  }
96
100
 
97
-
98
-
99
101
  fun setAdvancedText(text: String) {
100
102
  if (currentText == text) {
101
103
  Log.d(TAG, "Text unchanged, skipping update")
@@ -113,7 +115,6 @@ class AdvancedTextView : TextView {
113
115
  updateTextWithHighlights()
114
116
  }
115
117
 
116
-
117
118
  fun setAdvancedTextSize(size: Float) {
118
119
  if (fontSize == size) return
119
120
  fontSize = size
@@ -137,6 +138,13 @@ class AdvancedTextView : TextView {
137
138
  }
138
139
  }
139
140
 
141
+ fun setAdvancedLineHeight(multiplier: Float) {
142
+ if (lineHeightMultiplier == multiplier) return
143
+ lineHeightMultiplier = multiplier
144
+ updateTextWithHighlights()
145
+ }
146
+
147
+
140
148
  fun setAdvancedFontFamily(family: String) {
141
149
  if (fontFamily == family) return
142
150
  fontFamily = family
@@ -210,6 +218,7 @@ class AdvancedTextView : TextView {
210
218
  )
211
219
  }
212
220
 
221
+ // Add clickable span for word clicks
213
222
  spannableString.setSpan(
214
223
  WordClickableSpan(wordPos.index, wordPos.word),
215
224
  wordPos.start,
@@ -233,6 +242,11 @@ class AdvancedTextView : TextView {
233
242
  else -> Typeface.create(fontFamily, Typeface.NORMAL)
234
243
  }
235
244
 
245
+
246
+ lineSpacingMultiplier = lineHeightMultiplier
247
+ setLineSpacing(0f, lineHeightMultiplier)
248
+
249
+
236
250
  post {
237
251
  setText(spannableString, BufferType.SPANNABLE)
238
252
  Log.d(TAG, "Text updated with ${wordPositions.size} spans")
@@ -264,16 +278,13 @@ class AdvancedTextView : TextView {
264
278
  }
265
279
 
266
280
  private inner class WordClickableSpan(
267
- private val wordIndex: Int,
268
- private val word: String
281
+ private val wordIndex: Int,
282
+ private val word: String
269
283
  ) : ClickableSpan() {
270
284
 
271
285
  override fun onClick(widget: View) {
272
- Log.d(TAG, "WordClickableSpan onClick triggered: '$word' (index=$wordIndex)")
273
-
274
- widget.post {
275
- sendWordPressEvent(word, wordIndex)
276
- }
286
+ Log.d(TAG, "Word clicked: '$word' (index=$wordIndex)")
287
+ sendWordPressEvent(word, wordIndex)
277
288
  }
278
289
 
279
290
  override fun updateDrawState(ds: TextPaint) {
@@ -304,6 +315,56 @@ class AdvancedTextView : TextView {
304
315
  }
305
316
  }
306
317
 
318
+
319
+ private object SmartMovementMethod : ArrowKeyMovementMethod() {
320
+
321
+ override fun onTouchEvent(widget: TextView?, buffer: Spannable?, event: MotionEvent?): Boolean {
322
+ if (event != null && widget != null && buffer != null) {
323
+ if (handleMotion(event, widget, buffer)) {
324
+ return true
325
+ }
326
+ }
327
+ return super.onTouchEvent(widget, buffer, event)
328
+ }
329
+
330
+ private fun handleMotion(event: MotionEvent, widget: TextView, buffer: Spannable): Boolean {
331
+ var handled = false
332
+
333
+ if (event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_UP) {
334
+ val target = Point().apply {
335
+ x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX
336
+ y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY
337
+ }
338
+
339
+ val line = widget.layout.getLineForVertical(target.y)
340
+ val offset = widget.layout.getOffsetForHorizontal(line, target.x.toFloat())
341
+
342
+ if (event.action == MotionEvent.ACTION_DOWN) {
343
+ handled = handled || buffer.execute<ClickableSpan>(offset) {
344
+ Selection.setSelection(buffer, buffer.getSpanStart(it), buffer.getSpanEnd(it))
345
+ }
346
+ }
347
+
348
+ if (event.action == MotionEvent.ACTION_UP) {
349
+ handled = handled || buffer.execute<ClickableSpan>(offset) {
350
+ it.onClick(widget)
351
+ }
352
+ }
353
+ }
354
+
355
+ return handled
356
+ }
357
+
358
+ private inline fun <reified T : Any> Spannable.execute(offset: Int, fn: (T) -> Unit): Boolean {
359
+ val spans = this.getSpans<T>(offset, offset)
360
+ if (spans.isNotEmpty()) {
361
+ spans.forEach(fn)
362
+ return true
363
+ }
364
+ return false
365
+ }
366
+ }
367
+
307
368
  data class WordPosition(
308
369
  val index: Int,
309
370
  val start: Int,
@@ -123,6 +123,15 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
123
123
  }
124
124
  }
125
125
 
126
+
127
+ @ReactProp(name = "lineHeight")
128
+ fun setLineHeight(view: AdvancedTextView?, lineHeight: Float) {
129
+ android.util.Log.d(NAME, "setLineHeight called with: $lineHeight")
130
+ if (lineHeight > 0) {
131
+ view?.setAdvancedLineHeight(lineHeight)
132
+ }
133
+ }
134
+
126
135
  override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
127
136
  return mapOf(
128
137
  "onWordPress" to mapOf("registrationName" to "onWordPress"),
@@ -23,6 +23,7 @@ using namespace facebook::react;
23
23
  @property (nonatomic, strong) UIColor *textColor;
24
24
  @property (nonatomic, strong) NSString *textAlign;
25
25
  @property (nonatomic, strong) NSString *fontFamily;
26
+ @property (nonatomic, assign) CGFloat lineHeight;
26
27
 
27
28
  - (void)handleCustomMenuAction:(UIMenuItem *)sender;
28
29
 
@@ -89,6 +90,7 @@ using namespace facebook::react;
89
90
  _textColor = [UIColor labelColor];
90
91
  _textAlign = @"left";
91
92
  _fontFamily = @"System";
93
+ _lineHeight = 0.0;
92
94
 
93
95
  [self setupTextView];
94
96
  [self setupGestureRecognizers];
@@ -220,6 +222,12 @@ using namespace facebook::react;
220
222
  indicatorChanged = YES;
221
223
  }
222
224
 
225
+ if (oldViewProps.lineHeight != newViewProps.lineHeight) {
226
+ NSLog(@"[AdvancedTextView] Updating lineHeight to: %f", newViewProps.lineHeight);
227
+ _lineHeight = static_cast<CGFloat>(newViewProps.lineHeight);
228
+ styleChanged = YES;
229
+ }
230
+
223
231
  if (textChanged) {
224
232
  NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
225
233
  [self updateTextContent:text];
@@ -356,7 +364,25 @@ using namespace facebook::react;
356
364
  NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
357
365
  initWithString:_textView.text];
358
366
 
367
+ if (_lineHeight > 0) {
368
+ NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
359
369
 
370
+ UIFont *font = nil;
371
+ if (_fontFamily && _fontFamily.length > 0) {
372
+ font = [UIFont fontWithName:_fontFamily size:_fontSize > 0 ? _fontSize : 16.0];
373
+ }
374
+ if (!font) {
375
+ font = [UIFont systemFontOfSize:_fontSize > 0 ? _fontSize : 16.0];
376
+ }
377
+
378
+ CGFloat lineSpacing = (_lineHeight * font.lineHeight) - font.lineHeight;
379
+ paragraphStyle.lineSpacing = lineSpacing;
380
+
381
+ [attributedString addAttribute:NSParagraphStyleAttributeName
382
+ value:paragraphStyle
383
+ range:NSMakeRange(0, attributedString.length)];
384
+ }
385
+
360
386
  UIFont *font = nil;
361
387
 
362
388
  if (_fontFamily && _fontFamily.length > 0) {
@@ -1,26 +1,27 @@
1
- /* eslint-disable prettier/prettier */
2
- import type { ViewProps } from 'react-native';
3
- import { codegenNativeComponent } from 'react-native';
4
- // @ts-ignore
5
- import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
6
-
7
- interface HighlightedWord {
8
- index: Int32;
9
- highlightColor: string;
10
- }
11
-
12
- export interface NativeProps extends ViewProps {
13
- text: string;
14
- highlightedWords?: ReadonlyArray<HighlightedWord>;
15
- menuOptions?: ReadonlyArray<string>;
16
- onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
17
- onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
18
- indicatorWordIndex?: Int32;
19
- fontSize?: Int32;
20
- fontWeight?: string;
21
- color?: string;
22
- textAlign?: string;
23
- fontFamily?: string;
24
- }
25
-
26
- export default codegenNativeComponent<NativeProps>('AdvancedTextView');
1
+ /* eslint-disable prettier/prettier */
2
+ import type { ViewProps } from 'react-native';
3
+ import { codegenNativeComponent } from 'react-native';
4
+ // @ts-ignore
5
+ import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
6
+
7
+ interface HighlightedWord {
8
+ index: Int32;
9
+ highlightColor: string;
10
+ }
11
+
12
+ export interface NativeProps extends ViewProps {
13
+ text: string;
14
+ highlightedWords?: ReadonlyArray<HighlightedWord>;
15
+ menuOptions?: ReadonlyArray<string>;
16
+ onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
17
+ onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
18
+ indicatorWordIndex?: Int32;
19
+ fontSize?: Int32;
20
+ fontWeight?: string;
21
+ color?: string;
22
+ textAlign?: string;
23
+ fontFamily?: string;
24
+ lineHeight?: number;
25
+ }
26
+
27
+ export default codegenNativeComponent<NativeProps>('AdvancedTextView');
@@ -22,6 +22,7 @@ export interface NativeProps extends ViewProps {
22
22
  color?: string;
23
23
  textAlign?: string;
24
24
  fontFamily?: string;
25
+ lineHeight?: number;
25
26
  }
26
27
  declare const _default: import("react-native/types_generated/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeProps>;
27
28
  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,EAAE,MAAM,2CAA2C,CAAC;AAE3F,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;CACxB;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,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,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;CACrB;;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,EAAE,MAAM,2CAA2C,CAAC;AAE3F,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;CACxB;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,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,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,MAAM,CAAC;CACrB;;AAED,wBAAuE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-advanced-text",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
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",
@@ -1,26 +1,27 @@
1
- /* eslint-disable prettier/prettier */
2
- import type { ViewProps } from 'react-native';
3
- import { codegenNativeComponent } from 'react-native';
4
- // @ts-ignore
5
- import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
6
-
7
- interface HighlightedWord {
8
- index: Int32;
9
- highlightColor: string;
10
- }
11
-
12
- export interface NativeProps extends ViewProps {
13
- text: string;
14
- highlightedWords?: ReadonlyArray<HighlightedWord>;
15
- menuOptions?: ReadonlyArray<string>;
16
- onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
17
- onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
18
- indicatorWordIndex?: Int32;
19
- fontSize?: Int32;
20
- fontWeight?: string;
21
- color?: string;
22
- textAlign?: string;
23
- fontFamily?: string;
24
- }
25
-
26
- export default codegenNativeComponent<NativeProps>('AdvancedTextView');
1
+ /* eslint-disable prettier/prettier */
2
+ import type { ViewProps } from 'react-native';
3
+ import { codegenNativeComponent } from 'react-native';
4
+ // @ts-ignore
5
+ import type { DirectEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
6
+
7
+ interface HighlightedWord {
8
+ index: Int32;
9
+ highlightColor: string;
10
+ }
11
+
12
+ export interface NativeProps extends ViewProps {
13
+ text: string;
14
+ highlightedWords?: ReadonlyArray<HighlightedWord>;
15
+ menuOptions?: ReadonlyArray<string>;
16
+ onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
17
+ onSelection?: DirectEventHandler<{ selectedText: string; event: string }>;
18
+ indicatorWordIndex?: Int32;
19
+ fontSize?: Int32;
20
+ fontWeight?: string;
21
+ color?: string;
22
+ textAlign?: string;
23
+ fontFamily?: string;
24
+ lineHeight?: number;
25
+ }
26
+
27
+ export default codegenNativeComponent<NativeProps>('AdvancedTextView');