react-native-enriched 0.1.1 → 0.1.3

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,3 +1,5 @@
1
+ <img src="https://github.com/user-attachments/assets/abc75d3b-495b-4a76-a72f-d87ce3ca1ff9" alt="react-native-enriched by Software Mansion" width="100%">
2
+
1
3
  # react-native-enriched
2
4
 
3
5
  `react-native-enriched` is a powerful React Native library that exposes a rich text editor component:
@@ -19,6 +21,11 @@ Built by [Software Mansion](https://swmansion.com/) and sponsored by [Filament](
19
21
  &nbsp;&nbsp;&nbsp;
20
22
  <img width="80" height="80" alt="Filament Logo" src="https://github.com/user-attachments/assets/4103ab79-da34-4164-aa5f-dcf08815bf65" />
21
23
 
24
+ \
25
+ Since 2012 [Software Mansion](https://swmansion.com) is a software agency with experience in building web and mobile apps. We are Core React Native Contributors and experts in dealing with all kinds of React Native issues.
26
+ We can help you build your next dream product –
27
+ [Hire us](https://swmansion.com/contact/projects?utm_source=react-native-enriched&utm_medium=readme).
28
+
22
29
  ## Table of Contents
23
30
 
24
31
  - [Prerequisites](#prerequisites)
@@ -119,6 +126,7 @@ const styles = StyleSheet.create({
119
126
  alignItems: 'center',
120
127
  },
121
128
  input: {
129
+ width: '100%',
122
130
  fontSize: 20,
123
131
  padding: 10,
124
132
  maxHeight: 200,
@@ -131,27 +139,27 @@ const styles = StyleSheet.create({
131
139
  Summary of what happens here:
132
140
 
133
141
  1. Any methods imperatively called on the input to e.g. toggle some style must be used through a `ref` of `EnrichedTextInputInstance` type. Here, `toggleBold` method that is called on the button press calls `ref.current?.toggleBold()`, which toggles the bold styling within the current selection.
134
- 2. All the active styles info is emitted by `onChangeState` event. Set up a proper callback that accepts a `NativeSyntheticEvent<OnChangeStateEvent>` argument and you can access an object with boolean properties indicating which styles are active, such as `isBold` in the example. Here, this info is stored in a react state and used to change colors on the button.
142
+ 2. All the active styles info is emitted by `onChangeState` event. Set up a proper callback that accepts a `NativeSyntheticEvent<OnChangeStateEvent>` argument, and you can access an object with boolean properties indicating which styles are active, such as `isBold` in the example. Here, this info is stored in a React state and used to change colors on the button.
135
143
 
136
144
  ## Non Parametrized Styles
137
145
 
138
146
  Supported styles:
139
147
 
140
- - **bold**
141
- - *italic*
142
- - <ins>underline</ins>
143
- - ~~strikethrough~~
144
- - `inline code`
148
+ - bold
149
+ - italic
150
+ - underline
151
+ - strikethrough
152
+ - inline code
145
153
  - H1 heading
146
154
  - H2 heading
147
155
  - H3 heading
148
- - `codeblock`
149
- - > blockquote
156
+ - codeblock
157
+ - blockquote
150
158
  - ordered list
151
159
  - unordered list
152
160
 
153
161
  > [!NOTE]
154
- > The iOS doesn't support codeblocks just yet but it's planned in the near future!
162
+ > The iOS doesn't support codeblocks just yet, but it's planned in the near future!
155
163
 
156
164
  Each of the styles can be toggled the same way as in the example from [usage section](#usage); call a proper `toggle` function on the component ref.
157
165
 
@@ -168,7 +176,7 @@ The links are here, just like in any other editor, a piece of text with a URL at
168
176
 
169
177
  ### Automatic links detection
170
178
 
171
- `react-native-enriched` automatically detects words that appear to be some URLs and makes them links. Currently we are using pretty naive approach to detect whether text can be treated as a link or not. On iOS it's a pretty simple regex, on Android we are using URL regex provided by the system.
179
+ `react-native-enriched` automatically detects words that appear to be some URLs and makes them links. Currently, we are using pretty naive approach to detect whether text can be treated as a link or not. On iOS it's a pretty simple regex, on Android we are using URL regex provided by the system.
172
180
 
173
181
  ### Applying links manually
174
182
 
@@ -186,7 +194,7 @@ Mentions are meant to be a customisable style that lets you put mentioning phras
186
194
 
187
195
  ### Mention Indicators
188
196
 
189
- There is a [mentionIndicators](#mentionindicators) prop that lets you define what characters can start a mention. By default it is set to `[ @ ]`, meaning that typing a `@` character in the input will start the creation of a mention.
197
+ There is a [mentionIndicators](#mentionindicators) prop that lets you define what characters can start a mention. By default, it is set to `[ @ ]`, meaning that typing a `@` character in the input will start the creation of a mention.
190
198
 
191
199
  ### Starting a mention
192
200
 
@@ -208,10 +216,10 @@ Whenever you feel ready with the currently edited mention (so most likely user c
208
216
 
209
217
  You can insert an image into the input using [setImage](#setimage) ref method.
210
218
 
211
- The image will be put into a single line in the input and will affects the line's height as well as input's height. Keep in mind, that image will replace currently selected text or insert into the cursor position if there is no text selection.
219
+ The image will be put into a single line in the input and will affect the line's height as well as input's height. Keep in mind, that image will replace currently selected text or insert into the cursor position if there is no text selection.
212
220
 
213
221
  > [!NOTE]
214
- > The iOS doesn't support inline images just yet but it's planned in the near future!
222
+ > The iOS doesn't support inline images just yet, but it's planned in the near future!
215
223
 
216
224
  ## Style Detection
217
225
 
@@ -286,7 +294,7 @@ If `false`, text is not editable.
286
294
 
287
295
  #### `htmlStyle`
288
296
 
289
- A prop for customizing styles' appearances.
297
+ A prop for customizing styles appearances.
290
298
 
291
299
  | Type | Default Value | Platform |
292
300
  |--------------------------------|----------------------------------------------------|----------|
@@ -322,9 +330,9 @@ interface OnChangeHtmlEvent {
322
330
 
323
331
  - `value` is the new HTML.
324
332
 
325
- | Type | Default Value | Platform |
326
- |------------------------------------------------------|---------------|----------|
327
- | `(NativeSyntheticEvent\<OnChangeHtmlEvent>) => void` | - | Both |
333
+ | Type | Default Value | Platform |
334
+ |------------------------------------------------------------|---------------|----------|
335
+ | `(event: NativeSyntheticEvent<OnChangeHtmlEvent>) => void` | - | Both |
328
336
 
329
337
  #### `onChangeMention`
330
338
 
@@ -342,9 +350,9 @@ interface OnChangeMentionEvent {
342
350
  - `indicator` is the indicator of the currently edited mention.
343
351
  - `text` contains whole text that has been typed after the indicator.
344
352
 
345
- | Type | Default Value | Platform |
346
- |----------------------------------|---------------|----------|
347
- | `(OnChangeMentionEvent) => void` | - | Both |
353
+ | Type | Default Value | Platform |
354
+ |-----------------------------------------|---------------|----------|
355
+ | `(event: OnChangeMentionEvent) => void` | - | Both |
348
356
 
349
357
  #### `onChangeSelection`
350
358
 
@@ -353,7 +361,7 @@ Callback that is called each time user changes selection or moves the cursor in
353
361
  Payload interface:
354
362
 
355
363
  ```ts
356
- OnChangeSelectionEvent {
364
+ interface OnChangeSelectionEvent {
357
365
  start: Int32;
358
366
  end: Int32;
359
367
  text: string;
@@ -364,9 +372,9 @@ OnChangeSelectionEvent {
364
372
  - `end` is the first index after the selection's ending. For just a cursor in place (no selection), `start` equals `end`.
365
373
  - `text` is the input's text in the current selection.
366
374
 
367
- | Type | Default Value | Platform |
368
- |-----------------------------------------------------------|---------------|----------|
369
- | `(NativeSyntheticEvent\<OnChangeSelectionEvent>) => void` | - | Both |
375
+ | Type | Default Value | Platform |
376
+ |-----------------------------------------------------------------|---------------|----------|
377
+ | `(event: NativeSyntheticEvent<OnChangeSelectionEvent>) => void` | - | Both |
370
378
 
371
379
  #### `onChangeState`
372
380
 
@@ -394,9 +402,9 @@ interface OnChangeStateEvent {
394
402
  }
395
403
  ```
396
404
 
397
- | Type | Default Value | Platform |
398
- |-------------------------------------------------------|---------------|----------|
399
- | `(NativeSyntheticEvent\<OnChangeStateEvent>) => void` | - | Both |
405
+ | Type | Default Value | Platform |
406
+ |-------------------------------------------------------------|---------------|----------|
407
+ | `(event: NativeSyntheticEvent<OnChangeStateEvent>) => void` | - | Both |
400
408
 
401
409
  #### `onChangeText`
402
410
 
@@ -412,9 +420,9 @@ interface OnChangeTextEvent {
412
420
 
413
421
  - `value` is the new text value of the input.
414
422
 
415
- | Type | Default Value | Platform |
416
- |------------------------------------------------------|---------------|----------|
417
- | `(NativeSyntheticEvent\<OnChangeTextEvent>) => void` | - | Both |
423
+ | Type | Default Value | Platform |
424
+ |------------------------------------------------------------|---------------|----------|
425
+ | `(event: NativeSyntheticEvent<OnChangeTextEvent>) => void` | - | Both |
418
426
 
419
427
  #### `onEndMention`
420
428
 
@@ -454,9 +462,9 @@ interface OnLinkDetected {
454
462
  - `start` is the starting index of the link.
455
463
  - `end` is the first index after the ending index of the link.
456
464
 
457
- | Type | Default Value | Platform |
458
- |----------------------------|---------------|----------|
459
- | `(OnLinkDetected) => void` | - | Both |
465
+ | Type | Default Value | Platform |
466
+ |-----------------------------------|---------------|----------|
467
+ | `(event: OnLinkDetected) => void` | - | Both |
460
468
 
461
469
  #### `onMentionDetected`
462
470
 
@@ -465,7 +473,7 @@ Callback called when mention has been detected - either a new mention has been a
465
473
  Payload interface contains all the useful mention data:
466
474
 
467
475
  ```ts
468
- OnMentionDetected {
476
+ interface OnMentionDetected {
469
477
  text: string;
470
478
  indicator: string;
471
479
  attributes: Record<string, string>;
@@ -476,9 +484,9 @@ OnMentionDetected {
476
484
  - `indicator` is the indicator of the mention.
477
485
  - `attributes` are the additional user-defined attributes that are being stored with the mention.
478
486
 
479
- | Type | Default Value | Platform |
480
- |-------------------------------|---------------|----------|
481
- | `(OnMentionDetected) => void` | - | Both |
487
+ | Type | Default Value | Platform |
488
+ |--------------------------------------|---------------|----------|
489
+ | `(event: OnMentionDetected) => void` | - | Both |
482
490
 
483
491
  #### `onStartMention`
484
492
 
@@ -552,12 +560,12 @@ If true, Android will use experimental synchronous events. This will prevent fro
552
560
 
553
561
  ### Ref Methods
554
562
 
555
- All of the methods should be called on the input's [ref](#ref).
563
+ All the methods should be called on the input's [ref](#ref).
556
564
 
557
565
  #### `.blur()`
558
566
 
559
567
  ```ts
560
- blur: () => void
568
+ blur: () => void;
561
569
  ```
562
570
 
563
571
  Blurs the input.
@@ -804,6 +812,9 @@ interface MentionStyleProperties {
804
812
  - `fontSize` is the size of the heading's font, defaults to `32`/`24`/`20` for h1/h2/h3.
805
813
  - `bold` defines whether the heading should be bolded, defaults to `false`.
806
814
 
815
+ > [!NOTE]
816
+ > On iOS, the headings cannot have same `fontSize` as the component's `fontSize`. Doing so results in unexpected behavior.
817
+
807
818
  #### blockquote
808
819
 
809
820
  - `borderColor` defines the color of the rectangular border drawn to the left of blockquote text. Takes [color](https://reactnative.dev/docs/colors) value, defaults to `darkgray`.
@@ -204,7 +204,9 @@ class EnrichedTextInputView : AppCompatEditText {
204
204
  }
205
205
  }
206
206
 
207
- val finalText = currentText.mergeSpannables(start, end, item?.text.toString())
207
+ // Currently, we do not support pasting images
208
+ if (item?.text == null) return
209
+ val finalText = currentText.mergeSpannables(start, end, item.text.toString())
208
210
  setValue(finalText)
209
211
  parametrizedStyles?.detectAllLinks()
210
212
  }
@@ -154,6 +154,8 @@ class HtmlStyle {
154
154
  }
155
155
 
156
156
  private fun withOpacity(color: Int, alpha: Int): Int {
157
+ // Do not apply opacity to transparent color
158
+ if (Color.alpha(color) == 0) return color
157
159
  val a = alpha.coerceIn(0, 255)
158
160
  return (color and 0x00FFFFFF) or (a shl 24)
159
161
  }
@@ -148,7 +148,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
148
148
  mentionStart = start
149
149
  }
150
150
 
151
- mentionHandler.onMention(indicator, word.replaceFirst(indicator, ""))
151
+ mentionHandler.onMention(indicator, text)
152
152
  } else {
153
153
  mentionHandler.endMention()
154
154
  }
@@ -208,6 +208,11 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
208
208
  val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
209
209
  spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
210
210
 
211
+ val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' '
212
+ if (!hasSpaceAtTheEnd) {
213
+ spannable.insert(safeEnd, " ")
214
+ }
215
+
211
216
  view.selection.validateStyles()
212
217
  }
213
218
 
@@ -76,7 +76,7 @@
76
76
 
77
77
  - (void)tryHandlingPlainTextItemsIn:(UIPasteboard *)pasteboard range:(NSRange)range input:(EnrichedTextInputView *)input {
78
78
  NSArray *existingTypes = pasteboard.pasteboardTypes;
79
- NSArray *handledTypes = @[UTTypeUTF8PlainText.identifier, UTTypePlainText.identifier];
79
+ NSArray *handledTypes = @[UTTypeUTF8PlainText.identifier, UTTypePlainText.identifier, UTTypeURL.identifier];
80
80
  NSString *plainText;
81
81
 
82
82
  for(NSString *type in handledTypes) {
@@ -90,6 +90,8 @@
90
90
  plainText = [[NSString alloc]initWithData:value encoding:NSUTF8StringEncoding];
91
91
  } else if([value isKindOfClass:[NSString class]]) {
92
92
  plainText = (NSString *)value;
93
+ } else if([value isKindOfClass:[NSURL class]]) {
94
+ plainText = [(NSURL *)value absoluteString];
93
95
  }
94
96
  }
95
97
 
@@ -3,6 +3,7 @@
3
3
  #import "FontExtension.h"
4
4
  #import "OccurenceUtils.h"
5
5
  #import "ParagraphsUtils.h"
6
+ #import "ColorExtension.h"
6
7
 
7
8
  @implementation InlineCodeStyle {
8
9
  EnrichedTextInputView *_input;
@@ -33,7 +34,7 @@
33
34
  NSRange currentRange = [value rangeValue];
34
35
  [_input->textView.textStorage beginEditing];
35
36
 
36
- [_input->textView.textStorage addAttribute:NSBackgroundColorAttributeName value:[[_input->config inlineCodeBgColor] colorWithAlphaComponent:0.4] range:currentRange];
37
+ [_input->textView.textStorage addAttribute:NSBackgroundColorAttributeName value:[[_input->config inlineCodeBgColor] colorWithAlphaIfNotTransparent:0.4] range:currentRange];
37
38
  [_input->textView.textStorage addAttribute:NSForegroundColorAttributeName value:[_input->config inlineCodeFgColor] range:currentRange];
38
39
  [_input->textView.textStorage addAttribute:NSUnderlineColorAttributeName value:[_input->config inlineCodeFgColor] range:currentRange];
39
40
  [_input->textView.textStorage addAttribute:NSStrikethroughColorAttributeName value:[_input->config inlineCodeFgColor] range:currentRange];
@@ -41,7 +42,7 @@
41
42
  usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
42
43
  UIFont *font = (UIFont *)value;
43
44
  if(font != nullptr) {
44
- UIFont *newFont = [[_input->config monospacedFont] withFontTraits:font];
45
+ UIFont *newFont = [[[_input->config monospacedFont] withFontTraits:font] setSize:font.pointSize];
45
46
  [_input->textView.textStorage addAttribute:NSFontAttributeName value:newFont range:range];
46
47
  }
47
48
  }
@@ -53,13 +54,13 @@
53
54
 
54
55
  - (void)addTypingAttributes {
55
56
  NSMutableDictionary *newTypingAttrs = [_input->textView.typingAttributes mutableCopy];
56
- newTypingAttrs[NSBackgroundColorAttributeName] = [[_input->config inlineCodeBgColor] colorWithAlphaComponent:0.4];
57
+ newTypingAttrs[NSBackgroundColorAttributeName] = [[_input->config inlineCodeBgColor] colorWithAlphaIfNotTransparent:0.4];
57
58
  newTypingAttrs[NSForegroundColorAttributeName] = [_input->config inlineCodeFgColor];
58
59
  newTypingAttrs[NSUnderlineColorAttributeName] = [_input->config inlineCodeFgColor];
59
60
  newTypingAttrs[NSStrikethroughColorAttributeName] = [_input->config inlineCodeFgColor];
60
61
  UIFont* currentFont = (UIFont *)newTypingAttrs[NSFontAttributeName];
61
62
  if(currentFont != nullptr) {
62
- newTypingAttrs[NSFontAttributeName] = [[_input->config monospacedFont] withFontTraits:currentFont];
63
+ newTypingAttrs[NSFontAttributeName] = [[[_input->config monospacedFont] withFontTraits:currentFont] setSize:currentFont.pointSize];
63
64
  }
64
65
  _input->textView.typingAttributes = newTypingAttrs;
65
66
  }
@@ -75,7 +76,7 @@
75
76
  usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
76
77
  UIFont *font = (UIFont *)value;
77
78
  if(font != nullptr) {
78
- UIFont *newFont = [[_input->config primaryFont] withFontTraits:font];
79
+ UIFont *newFont = [[[_input->config primaryFont] withFontTraits:font] setSize:font.pointSize];
79
80
  [_input->textView.textStorage addAttribute:NSFontAttributeName value:newFont range:range];
80
81
  }
81
82
  }
@@ -92,7 +93,7 @@
92
93
  newTypingAttrs[NSStrikethroughColorAttributeName] = [_input->config primaryColor];
93
94
  UIFont* currentFont = (UIFont *)newTypingAttrs[NSFontAttributeName];
94
95
  if(currentFont != nullptr) {
95
- newTypingAttrs[NSFontAttributeName] = [[_input->config primaryFont] withFontTraits:currentFont];
96
+ newTypingAttrs[NSFontAttributeName] = [[[_input->config primaryFont] withFontTraits:currentFont] setSize:currentFont.pointSize];
96
97
  }
97
98
  _input->textView.typingAttributes = newTypingAttrs;
98
99
  }
@@ -4,6 +4,7 @@
4
4
  #import "TextInsertionUtils.h"
5
5
  #import "WordsUtils.h"
6
6
  #import "UIView+React.h"
7
+ #import "ColorExtension.h"
7
8
 
8
9
  // custom NSAttributedStringKey to differentiate from links
9
10
  static NSString *const MentionAttributeName = @"MentionAttributeName";
@@ -154,7 +155,7 @@ static NSString *const MentionAttributeName = @"MentionAttributeName";
154
155
  NSForegroundColorAttributeName: styleProps.color,
155
156
  NSUnderlineColorAttributeName: styleProps.color,
156
157
  NSStrikethroughColorAttributeName: styleProps.color,
157
- NSBackgroundColorAttributeName: [styleProps.backgroundColor colorWithAlphaComponent:0.4],
158
+ NSBackgroundColorAttributeName: [styleProps.backgroundColor colorWithAlphaIfNotTransparent:0.4],
158
159
  } mutableCopy];
159
160
 
160
161
  if(styleProps.decorationLine == DecorationUnderline) {
@@ -186,7 +187,7 @@ static NSString *const MentionAttributeName = @"MentionAttributeName";
186
187
  NSForegroundColorAttributeName: styleProps.color,
187
188
  NSUnderlineColorAttributeName: styleProps.color,
188
189
  NSStrikethroughColorAttributeName: styleProps.color,
189
- NSBackgroundColorAttributeName: [styleProps.backgroundColor colorWithAlphaComponent:0.4],
190
+ NSBackgroundColorAttributeName: [styleProps.backgroundColor colorWithAlphaIfNotTransparent:0.4],
190
191
  } mutableCopy];
191
192
 
192
193
  if(styleProps.decorationLine == DecorationUnderline) {
@@ -3,4 +3,5 @@
3
3
 
4
4
  @interface UIColor (ColorExtension)
5
5
  - (BOOL)isEqualToColor:(UIColor *)otherColor;
6
+ - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha;
6
7
  @end
@@ -24,4 +24,13 @@
24
24
 
25
25
  return [selfColor isEqual:otherColor];
26
26
  }
27
+
28
+ - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha {
29
+ CGFloat alpha = 0.0;
30
+ [self getRed:nil green:nil blue:nil alpha:&alpha];
31
+ if (alpha > 0.0) {
32
+ return [self colorWithAlphaComponent:newAlpha];
33
+ }
34
+ return self;
35
+ }
27
36
  @end
@@ -18,7 +18,7 @@
18
18
  [textView.textStorage insertAttributedString:newAttrStr atIndex:index];
19
19
 
20
20
  if(withSelection) {
21
- if(!textView.focused) {
21
+ if(![textView isFirstResponder]) {
22
22
  [textView reactFocus];
23
23
  }
24
24
  textView.selectedRange = NSMakeRange(index + text.length, 0);
@@ -38,7 +38,7 @@
38
38
  }
39
39
 
40
40
  if(withSelection) {
41
- if(!textView.focused) {
41
+ if(![textView isFirstResponder]) {
42
42
  [textView reactFocus];
43
43
  }
44
44
  textView.selectedRange = NSMakeRange(range.location + text.length, 0);
@@ -63,8 +63,7 @@
63
63
  }
64
64
 
65
65
  // fix the selection if needed
66
- if(input->textView.focused) {
67
- [input->textView reactFocus];
66
+ if([input->textView isFirstResponder]) {
68
67
  input->textView.selectedRange = NSMakeRange(preRemoveSelection.location + postRemoveOffset, preRemoveSelection.length);
69
68
  }
70
69
  }
@@ -112,8 +111,7 @@
112
111
  }
113
112
 
114
113
  // fix the selection if needed
115
- if(input->textView.focused) {
116
- [input->textView reactFocus];
114
+ if([input->textView isFirstResponder]) {
117
115
  input->textView.selectedRange = NSMakeRange(preAddSelection.location + postAddOffset, preAddSelection.length);
118
116
  }
119
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-enriched",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Rich Text Editor component for React Native",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",