react-native-enriched-markdown-scaffold 0.7.4-scaffold.1 → 0.7.4-scaffold.2

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.
@@ -848,18 +848,36 @@ class EnrichedMarkdownTextInputView(
848
848
  return true
849
849
  }
850
850
 
851
- private fun computeChipBounds(
851
+ /** Chip glyph rect in [Layout] coordinates — no padding, no scroll offset. */
852
+ private fun chipBoundsInLayout(
852
853
  layout: Layout,
853
854
  start: Int,
854
855
  ): Rect {
855
856
  val line = layout.getLineForOffset(start)
856
- val xStart = (layout.getPrimaryHorizontal(start)).toInt() + totalPaddingLeft - scrollX
857
- val xEnd = (layout.getPrimaryHorizontal(start + 1)).toInt() + totalPaddingLeft - scrollX
858
- val top = layout.getLineTop(line) + totalPaddingTop - scrollY
859
- val bottom = layout.getLineBottom(line) + totalPaddingTop - scrollY
860
- return Rect(xStart, top, xEnd, bottom)
857
+ val xStart = layout.getPrimaryHorizontal(start).toInt()
858
+ val xEnd = layout.getPrimaryHorizontal(start + 1).toInt()
859
+ // Ordered, not raw start/end: in an RTL run xStart > xEnd, which would make an
860
+ // inverted Rect that contains() always rejects.
861
+ val left = xStart.coerceAtMost(xEnd)
862
+ var right = xStart.coerceAtLeast(xEnd)
863
+ if (right <= left) {
864
+ // A chip that is the last character has start + 1 == length, where some layouts
865
+ // report the same horizontal for both offsets. Fall back to the line's content edge
866
+ // so the rect keeps a real width instead of collapsing.
867
+ right = layout.getLineRight(line).toInt()
868
+ }
869
+ return Rect(left, layout.getLineTop(line), right, layout.getLineBottom(line))
861
870
  }
862
871
 
872
+ /** Chip glyph rect in view coordinates, for reporting to JS. */
873
+ private fun computeChipBounds(
874
+ layout: Layout,
875
+ start: Int,
876
+ ): Rect =
877
+ chipBoundsInLayout(layout, start).apply {
878
+ offset(totalPaddingLeft - scrollX, totalPaddingTop - scrollY)
879
+ }
880
+
863
881
  fun mathChipPositionAtTouch(
864
882
  x: Float,
865
883
  y: Float,
@@ -869,13 +887,21 @@ class EnrichedMarkdownTextInputView(
869
887
  val layoutNow = layout ?: return -1
870
888
  val adjustedX = (x - totalPaddingLeft + scrollX).toInt()
871
889
  val adjustedY = (y - totalPaddingTop + scrollY).toInt()
872
- if (adjustedY < 0) return -1
890
+ // getLineForVertical and getOffsetForHorizontal both clamp into the laid-out text, so
891
+ // a touch in the padding or in the blank space below the last line resolves onto the
892
+ // nearest character. Bound the touch to the text box, then require it to land inside
893
+ // the chip's own rect — otherwise a tap beside a chip reads as a tap on it.
894
+ if (adjustedY < 0 || adjustedY > layoutNow.height) return -1
873
895
  val line = layoutNow.getLineForVertical(adjustedY)
874
896
  val offset = layoutNow.getOffsetForHorizontal(line, adjustedX.toFloat())
875
- if (offset < 0 || offset >= editable.length) return -1
897
+ if (offset < 0) return -1
876
898
  val candidates = intArrayOf(offset, offset - 1).filter { it in 0 until editable.length }
877
899
  for (candidate in candidates) {
878
900
  val range = controller.chipRangeAt(editable, candidate) ?: continue
901
+ // An empty rect means the layout could not give us the chip's extent; accept the
902
+ // candidate then, rather than making the chip untappable.
903
+ val bounds = chipBoundsInLayout(layoutNow, range.first)
904
+ if (!bounds.isEmpty && !bounds.contains(adjustedX, adjustedY)) continue
879
905
  return range.first
880
906
  }
881
907
  return -1
@@ -253,6 +253,20 @@ using namespace facebook::react;
253
253
 
254
254
  #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
255
255
 
256
+ // Glyph rect of the chip at `index`, in _textView coordinates (the same space
257
+ // `locationInView:_textView` reports). Single source of truth for both the tap
258
+ // hit-test and the rect reported to JS.
259
+ - (CGRect)mathChipRectInTextViewAtIndex:(NSUInteger)index layoutManager:(NSLayoutManager *)lm
260
+ {
261
+ if (lm == nil)
262
+ return CGRectNull;
263
+ NSRange glyphRange = [lm glyphRangeForCharacterRange:NSMakeRange(index, 1) actualCharacterRange:NULL];
264
+ CGRect rect = [lm boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
265
+ rect.origin.x += _textView.textContainerInset.left;
266
+ rect.origin.y += _textView.textContainerInset.top;
267
+ return rect;
268
+ }
269
+
256
270
  - (void)handleTapForMathChip:(UITapGestureRecognizer *)gesture
257
271
  {
258
272
  if (gesture.state != UIGestureRecognizerStateRecognized)
@@ -265,23 +279,28 @@ using namespace facebook::react;
265
279
  if (idx < 0)
266
280
  return;
267
281
  NSTextStorage *storage = _textView.textStorage;
268
- // The closest position can land just past the chip; probe both `idx` and `idx-1`.
282
+ NSLayoutManager *lm = storage.layoutManagers.firstObject;
283
+ // The closest position can land just past the chip, so probe both `idx` and `idx-1`.
284
+ // `closestPositionToPoint:` has no distance bound — it clamps a tap anywhere in the
285
+ // text view (blank space past the text, textContainerInset padding) onto the nearest
286
+ // character. Requiring the touch to fall inside the chip's own glyph rect is what
287
+ // keeps a tap beside a chip from reading as a tap on it.
269
288
  ENRMMathInlineAttachment *att = nil;
270
289
  NSUInteger chipIdx = NSNotFound;
271
- if ((NSUInteger)idx < storage.length) {
272
- id v = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)idx effectiveRange:NULL];
273
- if ([v isKindOfClass:[ENRMMathInlineAttachment class]]) {
274
- att = v;
275
- chipIdx = (NSUInteger)idx;
276
- }
277
- }
278
- if (!att && idx > 0) {
279
- NSUInteger probe = (NSUInteger)idx - 1;
280
- id v = [storage attribute:NSAttachmentAttributeName atIndex:probe effectiveRange:NULL];
281
- if ([v isKindOfClass:[ENRMMathInlineAttachment class]]) {
282
- att = v;
283
- chipIdx = probe;
284
- }
290
+ for (NSInteger probe = idx; probe >= idx - 1; probe--) {
291
+ if (probe < 0 || (NSUInteger)probe >= storage.length)
292
+ continue;
293
+ id v = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)probe effectiveRange:NULL];
294
+ if (![v isKindOfClass:[ENRMMathInlineAttachment class]])
295
+ continue;
296
+ // An empty rect means the layout could not give us the chip's extent; accept the
297
+ // candidate then, rather than making the chip untappable.
298
+ CGRect rect = [self mathChipRectInTextViewAtIndex:(NSUInteger)probe layoutManager:lm];
299
+ if (!CGRectIsEmpty(rect) && !CGRectContainsPoint(rect, loc))
300
+ continue;
301
+ att = v;
302
+ chipIdx = (NSUInteger)probe;
303
+ break;
285
304
  }
286
305
  if (!att)
287
306
  return;
@@ -290,7 +309,6 @@ using namespace facebook::react;
290
309
  [self clearMathChipSelectionsExcept:chipRange];
291
310
  att.isSelected = YES;
292
311
  [att invalidateRender];
293
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
294
312
  if (lm) {
295
313
  [lm invalidateLayoutForCharacterRange:chipRange actualCharacterRange:NULL];
296
314
  [lm ensureLayoutForCharacterRange:chipRange];
@@ -306,10 +324,8 @@ using namespace facebook::react;
306
324
  _suppressMathChipClearOnce = YES;
307
325
  _textView.selectedRange = NSMakeRange(caretIndex, 0);
308
326
 
309
- NSRange glyphRange = [lm glyphRangeForCharacterRange:chipRange actualCharacterRange:NULL];
310
- CGRect glyphRect = [lm boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
311
- glyphRect.origin.x += _textView.textContainerInset.left;
312
- glyphRect.origin.y += _textView.textContainerInset.top;
327
+ // Recomputed rather than reused from the hit-test: the layout was invalidated above.
328
+ CGRect glyphRect = [self mathChipRectInTextViewAtIndex:chipIdx layoutManager:lm];
313
329
  CGRect rectInSelf = [self.contentView convertRect:glyphRect fromView:_textView];
314
330
 
315
331
  [self emitOnMathPressWithLatex:att.latex start:chipIdx end:chipIdx + 1 displayMode:att.displayMode rect:rectInSelf];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-enriched-markdown-scaffold",
3
- "version": "0.7.4-scaffold.1",
3
+ "version": "0.7.4-scaffold.2",
4
4
  "description": "Markdown Text component for React Native",
5
5
  "main": "./lib/module/index",
6
6
  "module": "./lib/module/index",