@openeditor/react-native-prose-editor 0.0.8 → 0.0.10

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.
@@ -1,6 +1,6 @@
1
1
  import UIKit
2
2
 
3
- final class NativeBlockEditorSurface: UIScrollView {
3
+ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestureRecognizerDelegate {
4
4
  private final class TextLeafView: UITextView {
5
5
  struct PositionSegment {
6
6
  let range: NSRange
@@ -8,49 +8,34 @@ final class NativeBlockEditorSurface: UIScrollView {
8
8
  let docPos: UInt32
9
9
  let docSize: UInt32
10
10
  let isAtom: Bool
11
+ let nodeType: String?
11
12
  }
12
13
 
13
14
  var positionSegments: [PositionSegment] = []
14
- var onActivate: ((UInt32) -> Void)?
15
+ var compositionBaselineText: String?
15
16
  var geometryContainerWidth: CGFloat?
16
17
  private let geometryStorage = NSTextStorage()
17
18
  private let geometryLayoutManager = NSLayoutManager()
18
19
  private let geometryTextContainer = NSTextContainer()
19
20
  private var geometryInitialized = false
20
21
 
21
- // Leaves expose TextKit geometry but never own keyboard focus. The
22
- // shared EditorTextView input coordinator is the sole first responder.
23
- override var canBecomeFirstResponder: Bool { false }
24
-
25
22
  override var intrinsicContentSize: CGSize {
26
23
  let size = sizeThatFits(CGSize(width: bounds.width > 0 ? bounds.width : 320, height: .greatestFiniteMagnitude))
27
24
  return CGSize(width: UIView.noIntrinsicMetric, height: max(36, ceil(size.height)))
28
25
  }
29
26
 
30
- @objc func activate(_ gesture: UITapGestureRecognizer) {
31
- let point = gesture.location(in: self)
32
- var fraction: CGFloat = 0
33
- let glyphIndex = layoutManager.glyphIndex(
34
- for: point,
35
- in: textContainer,
36
- fractionOfDistanceThroughGlyph: &fraction
37
- )
38
- let utf16Offset = min(
39
- layoutManager.characterIndexForGlyph(at: glyphIndex) + (fraction >= 0.5 ? 1 : 0),
40
- textStorage.length
41
- )
27
+ func docPosition(forNativeOffset utf16Offset: Int) -> UInt32? {
42
28
  guard let segment = positionSegments.first(where: {
43
29
  utf16Offset >= $0.range.location && utf16Offset <= NSMaxRange($0.range)
44
- }) ?? positionSegments.last else { return }
30
+ }) ?? positionSegments.last else { return nil }
45
31
  if segment.isAtom {
46
32
  let after = utf16Offset > segment.range.location + segment.range.length / 2
47
- onActivate?(segment.docPos + (after ? segment.docSize : 0))
48
- return
33
+ return segment.docPos + (after ? segment.docSize : 0)
49
34
  }
50
35
  let localUtf16 = max(0, min(utf16Offset - segment.range.location, segment.text.utf16.count))
51
36
  let index = String.Index(utf16Offset: localUtf16, in: segment.text)
52
37
  let scalarOffset = segment.text[..<index].unicodeScalars.count
53
- onActivate?(segment.docPos + UInt32(scalarOffset))
38
+ return segment.docPos + UInt32(scalarOffset)
54
39
  }
55
40
 
56
41
  func nativeOffset(forDocPosition docPosition: UInt32) -> Int? {
@@ -214,6 +199,7 @@ final class NativeBlockEditorSurface: UIScrollView {
214
199
  }
215
200
 
216
201
  private let stack = UIStackView()
202
+ private let placeholderLabel = UILabel()
217
203
  private var currentEditorId: UInt64 = 0
218
204
  private var isApplyingRemoteUpdate = false
219
205
  private var currentTheme: EditorTheme?
@@ -230,7 +216,14 @@ final class NativeBlockEditorSurface: UIScrollView {
230
216
  private var textLeafViews: [TextLeafView] = []
231
217
  private var selectedNodePos: UInt32?
232
218
  private var selectedTextPos: UInt32?
233
- private let caretView = UIView()
219
+ private var isApplyingEditorUpdate = false
220
+
221
+ var placeholder: String = "" {
222
+ didSet {
223
+ placeholderLabel.text = placeholder
224
+ refreshPlaceholder()
225
+ }
226
+ }
234
227
 
235
228
  var baseFont: UIFont = .systemFont(ofSize: 16)
236
229
  var baseTextColor: UIColor = .label
@@ -240,18 +233,94 @@ final class NativeBlockEditorSurface: UIScrollView {
240
233
  }
241
234
  }
242
235
  var onAppliedUpdate: ((String) -> Void)?
243
- var onRequestTextInput: ((UInt32) -> Void)?
236
+ var onFocusChange: ((Bool) -> Void)?
237
+ private var configuredInputAccessoryView: UIView?
238
+ private var configuredAutocapitalizationType: UITextAutocapitalizationType = .sentences
239
+ private var configuredAutocorrectionType: UITextAutocorrectionType = .default
240
+ private var configuredKeyboardType: UIKeyboardType = .default
241
+ private var configuredKeyboardAppearance: UIKeyboardAppearance = .default
244
242
  var isEditable = true {
245
243
  didSet {
244
+ textLeafViews.forEach {
245
+ $0.isEditable = isEditable
246
+ $0.isSelectable = isEditable
247
+ }
246
248
  refreshImageSelection()
247
- refreshCaret()
248
249
  }
249
250
  }
250
- var isInputFocused = false {
251
- didSet { refreshCaret() }
252
- }
253
251
  var onContentHeightMayChange: ((CGFloat) -> Void)?
254
252
 
253
+ var activeTextView: UITextView? {
254
+ textLeafViews.first(where: { $0.isFirstResponder })
255
+ ?? textLeafViews.first(where: { leaf in
256
+ guard let selectedTextPos else { return false }
257
+ return leaf.nativeOffset(forDocPosition: selectedTextPos) != nil
258
+ })
259
+ ?? textLeafViews.first
260
+ }
261
+
262
+ func activeDocPosition(forUTF16Offset offset: Int) -> UInt32? {
263
+ guard let leaf = activeTextView as? TextLeafView else { return nil }
264
+ return leaf.docPosition(forNativeOffset: offset)
265
+ }
266
+
267
+ func activeInlineAtomType(atUTF16Offset offset: Int) -> String? {
268
+ guard let leaf = activeTextView as? TextLeafView else { return nil }
269
+ return leaf.positionSegments.first(where: {
270
+ $0.isAtom && offset >= $0.range.location && offset <= NSMaxRange($0.range)
271
+ })?.nodeType
272
+ }
273
+
274
+ var activeDocSelection: (anchor: UInt32, head: UInt32)? {
275
+ guard let leaf = activeTextView as? TextLeafView,
276
+ let range = leaf.selectedTextRange
277
+ else { return nil }
278
+ let start = leaf.offset(from: leaf.beginningOfDocument, to: range.start)
279
+ let end = leaf.offset(from: leaf.beginningOfDocument, to: range.end)
280
+ guard let anchor = leaf.docPosition(forNativeOffset: start),
281
+ let head = leaf.docPosition(forNativeOffset: end)
282
+ else { return nil }
283
+ return (anchor, head)
284
+ }
285
+
286
+ @discardableResult
287
+ func focus() -> Bool {
288
+ activeTextView?.becomeFirstResponder() ?? false
289
+ }
290
+
291
+ func blur() {
292
+ activeTextView?.resignFirstResponder()
293
+ }
294
+
295
+ func setInputAccessoryView(_ view: UIView?) {
296
+ configuredInputAccessoryView = view
297
+ textLeafViews.forEach { $0.inputAccessoryView = view }
298
+ activeTextView?.reloadInputViews()
299
+ }
300
+
301
+ func configureInputTraits(
302
+ autocapitalizationType: UITextAutocapitalizationType? = nil,
303
+ autocorrectionType: UITextAutocorrectionType? = nil,
304
+ keyboardType: UIKeyboardType? = nil,
305
+ keyboardAppearance: UIKeyboardAppearance? = nil
306
+ ) {
307
+ if let autocapitalizationType { configuredAutocapitalizationType = autocapitalizationType }
308
+ if let autocorrectionType { configuredAutocorrectionType = autocorrectionType }
309
+ if let keyboardType { configuredKeyboardType = keyboardType }
310
+ if let keyboardAppearance { configuredKeyboardAppearance = keyboardAppearance }
311
+ textLeafViews.forEach(applyInputConfiguration)
312
+ activeTextView?.reloadInputViews()
313
+ }
314
+
315
+ private func applyInputConfiguration(to leaf: TextLeafView) {
316
+ leaf.inputAccessoryView = configuredInputAccessoryView
317
+ leaf.autocapitalizationType = configuredAutocapitalizationType
318
+ leaf.autocorrectionType = configuredAutocorrectionType
319
+ leaf.spellCheckingType = configuredAutocorrectionType == .no ? .no : .default
320
+ leaf.keyboardType = configuredKeyboardType
321
+ leaf.keyboardAppearance = configuredKeyboardAppearance
322
+ }
323
+
255
324
  override init(frame: CGRect) {
256
325
  super.init(frame: frame)
257
326
  setup()
@@ -270,11 +339,11 @@ final class NativeBlockEditorSurface: UIScrollView {
270
339
  stack.spacing = 8
271
340
  stack.alignment = .fill
272
341
  addSubview(stack)
273
- caretView.backgroundColor = .systemBlue
274
- caretView.isUserInteractionEnabled = false
275
- caretView.layer.cornerRadius = 1
276
- caretView.isHidden = true
277
- addSubview(caretView)
342
+ placeholderLabel.numberOfLines = 0
343
+ placeholderLabel.isUserInteractionEnabled = false
344
+ placeholderLabel.isHidden = true
345
+ placeholderLabel.layer.zPosition = 1
346
+ addSubview(placeholderLabel)
278
347
  }
279
348
 
280
349
  override func layoutSubviews() {
@@ -297,20 +366,32 @@ final class NativeBlockEditorSurface: UIScrollView {
297
366
  ).height
298
367
  stack.frame.size.height = contentHeight
299
368
  stack.layoutIfNeeded()
369
+ placeholderLabel.font = font(for: ["nodeType": "paragraph"])
370
+ placeholderLabel.textColor = currentTheme?.placeholderColor ?? .placeholderText
371
+ placeholderLabel.frame = CGRect(
372
+ x: leftInset,
373
+ y: topInset,
374
+ width: contentWidth,
375
+ height: placeholderLabel.sizeThatFits(
376
+ CGSize(width: contentWidth, height: .greatestFiniteMagnitude)
377
+ ).height
378
+ )
300
379
  contentSize = CGSize(width: bounds.width, height: stack.frame.maxY + bottomInset)
301
- refreshCaret()
302
380
  onContentHeightMayChange?(contentSize.height)
303
381
  }
304
382
 
305
383
  func applyTheme(_ theme: EditorTheme?) {
306
384
  currentTheme = theme
385
+ backgroundColor = theme?.backgroundColor ?? baseBackgroundColor
307
386
  contentInset = .zero
308
387
  verticalScrollIndicatorInsets = .zero
309
- if let currentRoot {
310
- rebuildAll(from: currentRoot)
388
+ if let root = currentRoot {
389
+ rebuildAll(from: root)
390
+ currentRoot = root
311
391
  refreshTableCellRegistry()
312
392
  refreshImageSelection()
313
393
  }
394
+ refreshPlaceholder()
314
395
  setNeedsLayout()
315
396
  }
316
397
 
@@ -325,19 +406,30 @@ final class NativeBlockEditorSurface: UIScrollView {
325
406
  guard let data = updateJSON.data(using: .utf8),
326
407
  let update = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
327
408
  else { return }
409
+ let shouldPreserveTextFocus = textLeafViews.contains(where: \.isFirstResponder)
410
+ isApplyingEditorUpdate = true
411
+ defer { isApplyingEditorUpdate = false }
328
412
 
413
+ var textSelection: (UInt32, UInt32)?
329
414
  if let selection = update["selection"] as? [String: Any] {
330
415
  let type = selection["type"] as? String
331
416
  selectedNodePos = type == "node" ? (selection["pos"] as? NSNumber)?.uint32Value : nil
332
417
  let anchor = (selection["anchor"] as? NSNumber)?.uint32Value
333
418
  let head = (selection["head"] as? NSNumber)?.uint32Value
334
419
  selectedTextPos = type == "text" && anchor == head ? anchor : nil
420
+ if type == "text", let anchor, let head {
421
+ textSelection = (anchor, head)
422
+ }
335
423
  refreshImageSelection()
336
- refreshCaret()
337
424
  }
338
425
  guard let renderTree = update["renderTree"] as? [String: Any],
339
426
  let root = renderTree["root"] as? [String: Any]
340
- else { return }
427
+ else {
428
+ if let textSelection {
429
+ applyNativeSelection(anchor: textSelection.0, head: textSelection.1, preservingFocus: shouldPreserveTextFocus)
430
+ }
431
+ return
432
+ }
341
433
 
342
434
  currentEditorId = editorId
343
435
  isApplyingRemoteUpdate = true
@@ -349,9 +441,12 @@ final class NativeBlockEditorSurface: UIScrollView {
349
441
  }
350
442
  refreshTableCellRegistry()
351
443
  refreshImageSelection()
352
- refreshCaret()
353
444
  currentRoot = root
445
+ refreshPlaceholder()
354
446
  isApplyingRemoteUpdate = false
447
+ if let textSelection {
448
+ applyNativeSelection(anchor: textSelection.0, head: textSelection.1, preservingFocus: shouldPreserveTextFocus)
449
+ }
355
450
  setNeedsLayout()
356
451
  layoutIfNeeded()
357
452
  }
@@ -375,6 +470,22 @@ final class NativeBlockEditorSurface: UIScrollView {
375
470
  }
376
471
  }
377
472
 
473
+ private func refreshPlaceholder() {
474
+ placeholderLabel.isHidden = placeholder.isEmpty || !isRenderedDocumentEmpty()
475
+ setNeedsLayout()
476
+ }
477
+
478
+ private func isRenderedDocumentEmpty() -> Bool {
479
+ guard let root = currentRoot else { return false }
480
+ let children = root["children"] as? [[String: Any]] ?? []
481
+ guard children.count <= 1 else { return false }
482
+ guard let child = children.first else {
483
+ return textLeafViews.count == 1 && textLeafViews[0].attributedText.length == 0
484
+ }
485
+ guard (child["nodeType"] as? String ?? "paragraph") == "paragraph" else { return false }
486
+ return textLeafViews.count == 1 && textLeafViews[0].attributedText.length == 0
487
+ }
488
+
378
489
  /// Applies canonical recursive patches at the smallest native view boundary.
379
490
  /// Metadata-only changes mutate bindings in place; node replacements stop at
380
491
  /// the nearest rendered owner (normally the edited text block); and root
@@ -443,6 +554,10 @@ final class NativeBlockEditorSurface: UIScrollView {
443
554
  rebuildAll(from: root)
444
555
  return
445
556
  }
557
+ if let leaf = oldView as? TextLeafView {
558
+ configureTextLeaf(leaf, for: node)
559
+ return
560
+ }
446
561
  guard let parent = oldView.superview else {
447
562
  rebuildAll(from: root)
448
563
  return
@@ -478,7 +593,7 @@ final class NativeBlockEditorSurface: UIScrollView {
478
593
  let blockPos = (node["docPos"] as? NSNumber)?.uint32Value ?? 0
479
594
  let blockSize = (node["docSize"] as? NSNumber)?.uint32Value ?? 0
480
595
  leaf.positionSegments = [
481
- .init(range: NSRange(location: 0, length: 0), text: "", docPos: blockPos, docSize: blockSize, isAtom: false),
596
+ .init(range: NSRange(location: 0, length: 0), text: "", docPos: blockPos, docSize: blockSize, isAtom: false, nodeType: nil),
482
597
  ]
483
598
  } else {
484
599
  leaf.positionSegments = segments
@@ -546,6 +661,7 @@ final class NativeBlockEditorSurface: UIScrollView {
546
661
  view.removeFromSuperview()
547
662
  }
548
663
  currentRoot = nil
664
+ refreshPlaceholder()
549
665
  nodeViews.removeAll()
550
666
  tableCells.removeAll()
551
667
  imageViews.removeAll()
@@ -734,6 +850,14 @@ final class NativeBlockEditorSurface: UIScrollView {
734
850
  stack.arrangedSubviews.count
735
851
  }
736
852
 
853
+ func isPlaceholderVisibleForTesting() -> Bool {
854
+ !placeholderLabel.isHidden
855
+ }
856
+
857
+ func placeholderColorForTesting() -> UIColor? {
858
+ placeholderLabel.textColor
859
+ }
860
+
737
861
  func firstTextAttributesForTesting() -> [NSAttributedString.Key: Any]? {
738
862
  func find(in view: UIView) -> TextLeafView? {
739
863
  if let leaf = view as? TextLeafView, leaf.attributedText.length > 0 {
@@ -797,6 +921,7 @@ final class NativeBlockEditorSurface: UIScrollView {
797
921
  cell.backgroundColor = cell.baseFillColor
798
922
  cell.isUserInteractionEnabled = true
799
923
  let tap = UITapGestureRecognizer(target: self, action: #selector(handleCellTap(_:)))
924
+ tap.delegate = self
800
925
  cell.addGestureRecognizer(tap)
801
926
  tableCells.append(cell)
802
927
  for (index, child) in (node["children"] as? [[String: Any]] ?? []).enumerated() {
@@ -808,8 +933,25 @@ final class NativeBlockEditorSurface: UIScrollView {
808
933
  return cell
809
934
  }
810
935
 
936
+ func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
937
+ guard gestureRecognizer.view is CellView else { return true }
938
+ var touchedView: UIView? = touch.view
939
+ while let view = touchedView, view !== gestureRecognizer.view {
940
+ if view is UITextView || view is UIControl { return false }
941
+ touchedView = view.superview
942
+ }
943
+ return true
944
+ }
945
+
811
946
  private func textBlockView(for node: [String: Any], depth: Int) -> UIView {
812
947
  let leaf = TextLeafView()
948
+ configureTextLeaf(leaf, for: node)
949
+ return leaf
950
+ }
951
+
952
+ private func configureTextLeaf(_ leaf: TextLeafView, for node: [String: Any]) {
953
+ let wasFirstResponder = leaf.isFirstResponder
954
+ let previousSelection = leaf.selectedRange
813
955
  leaf.font = font(for: node)
814
956
  let nodeType = node["nodeType"] as? String ?? "paragraph"
815
957
  let textStyle = currentTheme?.effectiveTextStyle(for: nodeType)
@@ -844,9 +986,10 @@ final class NativeBlockEditorSurface: UIScrollView {
844
986
  .init(
845
987
  range: NSRange(location: 0, length: 0),
846
988
  text: "",
847
- docPos: blockPos,
848
- docSize: blockSize,
849
- isAtom: false
989
+ docPos: blockPos + (blockSize >= 2 ? 1 : 0),
990
+ docSize: 0,
991
+ isAtom: false,
992
+ nodeType: nil
850
993
  ),
851
994
  ]
852
995
  }
@@ -865,16 +1008,17 @@ final class NativeBlockEditorSurface: UIScrollView {
865
1008
  leaf.textContainerInset = .zero
866
1009
  }
867
1010
  leaf.isScrollEnabled = false
868
- leaf.isEditable = false
869
- leaf.isSelectable = false
870
- leaf.textContainer.widthTracksTextView = false
1011
+ leaf.isEditable = isEditable
1012
+ leaf.isSelectable = isEditable
1013
+ leaf.textContainer.widthTracksTextView = true
871
1014
  leaf.textContainer.lineFragmentPadding = 0
872
- leaf.onActivate = { [weak self] docPos in
873
- guard let self, self.isEditable else { return }
874
- self.onRequestTextInput?(docPos)
1015
+ leaf.delegate = self
1016
+ applyInputConfiguration(to: leaf)
1017
+ if wasFirstResponder {
1018
+ let location = min(previousSelection.location, leaf.textStorage.length)
1019
+ let length = min(previousSelection.length, leaf.textStorage.length - location)
1020
+ leaf.selectedRange = NSRange(location: location, length: length)
875
1021
  }
876
- leaf.addGestureRecognizer(UITapGestureRecognizer(target: leaf, action: #selector(TextLeafView.activate(_:))))
877
- return leaf
878
1022
  }
879
1023
 
880
1024
  private func attributedInlineContent(
@@ -901,7 +1045,8 @@ final class NativeBlockEditorSurface: UIScrollView {
901
1045
  text: value,
902
1046
  docPos: docPos,
903
1047
  docSize: docSize,
904
- isAtom: false
1048
+ isAtom: false,
1049
+ nodeType: nil
905
1050
  ))
906
1051
  return
907
1052
  }
@@ -925,7 +1070,8 @@ final class NativeBlockEditorSurface: UIScrollView {
925
1070
  text: value,
926
1071
  docPos: docPos,
927
1072
  docSize: max(docSize, 1),
928
- isAtom: true
1073
+ isAtom: true,
1074
+ nodeType: nodeType
929
1075
  ))
930
1076
  return
931
1077
  }
@@ -962,7 +1108,7 @@ final class NativeBlockEditorSurface: UIScrollView {
962
1108
  attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
963
1109
  }
964
1110
  if markTypes.contains("link") {
965
- attributes[.foregroundColor] = tintColor
1111
+ attributes[.foregroundColor] = currentTheme?.links?.color ?? tintColor
966
1112
  }
967
1113
  if markTypes.contains("code") {
968
1114
  attributes[.font] = UIFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
@@ -1135,16 +1281,132 @@ final class NativeBlockEditorSurface: UIScrollView {
1135
1281
  }
1136
1282
  }
1137
1283
 
1138
- private func refreshCaret() {
1139
- guard isEditable, isInputFocused, selectedNodePos == nil,
1140
- let rect = selectedCaretRect()
1284
+ private func applyNativeSelection(anchor: UInt32, head: UInt32, preservingFocus: Bool) {
1285
+ let candidates = textLeafViews.filter {
1286
+ $0.nativeOffset(forDocPosition: anchor) != nil &&
1287
+ $0.nativeOffset(forDocPosition: head) != nil
1288
+ }
1289
+ guard let leaf = candidates.first(where: \.isFirstResponder)
1290
+ ?? (anchor == head ? candidates.last : candidates.first),
1291
+ let anchorOffset = leaf.nativeOffset(forDocPosition: anchor),
1292
+ let headOffset = leaf.nativeOffset(forDocPosition: head),
1293
+ let start = leaf.position(
1294
+ from: leaf.beginningOfDocument,
1295
+ offset: min(anchorOffset, headOffset)
1296
+ ),
1297
+ let end = leaf.position(
1298
+ from: leaf.beginningOfDocument,
1299
+ offset: max(anchorOffset, headOffset)
1300
+ ),
1301
+ let range = leaf.textRange(from: start, to: end)
1302
+ else { return }
1303
+ leaf.selectedTextRange = range
1304
+ if preservingFocus, !leaf.isFirstResponder {
1305
+ leaf.becomeFirstResponder()
1306
+ }
1307
+ }
1308
+
1309
+ func textViewDidChangeSelection(_ textView: UITextView) {
1310
+ guard !isApplyingEditorUpdate, isEditable, currentEditorId != 0,
1311
+ let leaf = textView as? TextLeafView,
1312
+ let range = leaf.selectedTextRange
1313
+ else { return }
1314
+ let anchorOffset = leaf.offset(from: leaf.beginningOfDocument, to: range.start)
1315
+ let headOffset = leaf.offset(from: leaf.beginningOfDocument, to: range.end)
1316
+ guard let anchor = leaf.docPosition(forNativeOffset: anchorOffset),
1317
+ let head = leaf.docPosition(forNativeOffset: headOffset)
1318
+ else { return }
1319
+ editorSetSelection(id: currentEditorId, anchor: anchor, head: head)
1320
+ onAppliedUpdate?(editorGetSelectionState(id: currentEditorId))
1321
+ }
1322
+
1323
+ func textViewDidChange(_ textView: UITextView) {
1324
+ guard !isApplyingEditorUpdate, isEditable, currentEditorId != 0,
1325
+ let leaf = textView as? TextLeafView,
1326
+ textView.markedTextRange == nil,
1327
+ let baseline = leaf.compositionBaselineText
1328
+ else { return }
1329
+ leaf.compositionBaselineText = nil
1330
+ commitNativeText(in: leaf, baseline: baseline, current: leaf.text ?? "")
1331
+ }
1332
+
1333
+ func textViewDidBeginEditing(_ textView: UITextView) {
1334
+ onFocusChange?(true)
1335
+ }
1336
+
1337
+ func textViewDidEndEditing(_ textView: UITextView) {
1338
+ if !textLeafViews.contains(where: { $0.isFirstResponder }) {
1339
+ onFocusChange?(false)
1340
+ }
1341
+ }
1342
+
1343
+ func textView(
1344
+ _ textView: UITextView,
1345
+ shouldChangeTextIn range: NSRange,
1346
+ replacementText text: String
1347
+ ) -> Bool {
1348
+ guard !isApplyingEditorUpdate, isEditable, currentEditorId != 0,
1349
+ let leaf = textView as? TextLeafView
1350
+ else { return false }
1351
+
1352
+ if text.isEmpty, range.length == 0, range.location == 0,
1353
+ let docPosition = leaf.docPosition(forNativeOffset: 0) {
1354
+ let scalar = editorDocToScalar(id: currentEditorId, docPos: docPosition)
1355
+ let update = editorDeleteBackwardAtSelectionScalar(
1356
+ id: currentEditorId,
1357
+ scalarAnchor: scalar,
1358
+ scalarHead: scalar
1359
+ )
1360
+ applyUpdateJSON(update, editorId: currentEditorId)
1361
+ onAppliedUpdate?(update)
1362
+ return false
1363
+ }
1364
+
1365
+ if text == "\n" {
1366
+ guard let anchor = leaf.docPosition(forNativeOffset: range.location),
1367
+ let head = leaf.docPosition(forNativeOffset: NSMaxRange(range))
1368
+ else { return false }
1369
+ editorSetSelection(id: currentEditorId, anchor: anchor, head: head)
1370
+ if anchor != head {
1371
+ _ = editorReplaceSelectionText(id: currentEditorId, text: "")
1372
+ }
1373
+ let update = editorSplitBlock(id: currentEditorId, pos: anchor)
1374
+ applyUpdateJSON(update, editorId: currentEditorId)
1375
+ onAppliedUpdate?(update)
1376
+ return false
1377
+ }
1378
+
1379
+ if leaf.compositionBaselineText == nil {
1380
+ leaf.compositionBaselineText = leaf.text ?? ""
1381
+ }
1382
+ return true
1383
+ }
1384
+
1385
+ private func commitNativeText(in leaf: TextLeafView, baseline: String, current: String) {
1386
+ guard baseline != current else { return }
1387
+ let old = baseline as NSString
1388
+ let new = current as NSString
1389
+ var prefix = 0
1390
+ while prefix < old.length, prefix < new.length,
1391
+ old.character(at: prefix) == new.character(at: prefix) { prefix += 1 }
1392
+ var oldSuffix = old.length
1393
+ var newSuffix = new.length
1394
+ while oldSuffix > prefix, newSuffix > prefix,
1395
+ old.character(at: oldSuffix - 1) == new.character(at: newSuffix - 1) {
1396
+ oldSuffix -= 1
1397
+ newSuffix -= 1
1398
+ }
1399
+ guard let anchor = leaf.docPosition(forNativeOffset: prefix),
1400
+ let head = leaf.docPosition(forNativeOffset: oldSuffix)
1141
1401
  else {
1142
- caretView.isHidden = true
1402
+ applyUpdateJSON(editorGetCurrentState(id: currentEditorId), editorId: currentEditorId)
1143
1403
  return
1144
1404
  }
1145
- caretView.frame = CGRect(x: rect.minX, y: rect.minY, width: 2, height: rect.height)
1146
- caretView.isHidden = false
1147
- bringSubviewToFront(caretView)
1405
+ let replacement = new.substring(with: NSRange(location: prefix, length: newSuffix - prefix))
1406
+ editorSetSelection(id: currentEditorId, anchor: anchor, head: head)
1407
+ let update = editorReplaceSelectionText(id: currentEditorId, text: replacement)
1408
+ applyUpdateJSON(update, editorId: currentEditorId)
1409
+ onAppliedUpdate?(update)
1148
1410
  }
1149
1411
 
1150
1412
  func firstImageGeometryForTesting() -> (docPos: UInt32, rect: CGRect)? {