@openeditor/react-native-prose-editor 0.0.9 → 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.
- package/README.md +5 -9
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +23 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +381 -49
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +110 -169
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +124 -337
- package/dist/EditorTheme.d.ts +8 -0
- package/ios/NativeBlockEditorSurface.swift +268 -58
- package/ios/NativeEditorExpoView.swift +114 -117
- package/ios/NativeInputSupport.swift +0 -302
- package/ios/PositionBridge.swift +22 -548
- package/ios/RichTextEditorView.swift +217 -4671
- package/package.json +1 -1
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +0 -50
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +0 -4068
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +0 -1009
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +0 -6
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
return segment.docPos + UInt32(scalarOffset)
|
|
54
39
|
}
|
|
55
40
|
|
|
56
41
|
func nativeOffset(forDocPosition docPosition: UInt32) -> Int? {
|
|
@@ -231,7 +216,7 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
231
216
|
private var textLeafViews: [TextLeafView] = []
|
|
232
217
|
private var selectedNodePos: UInt32?
|
|
233
218
|
private var selectedTextPos: UInt32?
|
|
234
|
-
private
|
|
219
|
+
private var isApplyingEditorUpdate = false
|
|
235
220
|
|
|
236
221
|
var placeholder: String = "" {
|
|
237
222
|
didSet {
|
|
@@ -248,18 +233,94 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
248
233
|
}
|
|
249
234
|
}
|
|
250
235
|
var onAppliedUpdate: ((String) -> Void)?
|
|
251
|
-
var
|
|
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
|
|
252
242
|
var isEditable = true {
|
|
253
243
|
didSet {
|
|
244
|
+
textLeafViews.forEach {
|
|
245
|
+
$0.isEditable = isEditable
|
|
246
|
+
$0.isSelectable = isEditable
|
|
247
|
+
}
|
|
254
248
|
refreshImageSelection()
|
|
255
|
-
refreshCaret()
|
|
256
249
|
}
|
|
257
250
|
}
|
|
258
|
-
var isInputFocused = false {
|
|
259
|
-
didSet { refreshCaret() }
|
|
260
|
-
}
|
|
261
251
|
var onContentHeightMayChange: ((CGFloat) -> Void)?
|
|
262
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
|
+
|
|
263
324
|
override init(frame: CGRect) {
|
|
264
325
|
super.init(frame: frame)
|
|
265
326
|
setup()
|
|
@@ -283,11 +344,6 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
283
344
|
placeholderLabel.isHidden = true
|
|
284
345
|
placeholderLabel.layer.zPosition = 1
|
|
285
346
|
addSubview(placeholderLabel)
|
|
286
|
-
caretView.backgroundColor = .systemBlue
|
|
287
|
-
caretView.isUserInteractionEnabled = false
|
|
288
|
-
caretView.layer.cornerRadius = 1
|
|
289
|
-
caretView.isHidden = true
|
|
290
|
-
addSubview(caretView)
|
|
291
347
|
}
|
|
292
348
|
|
|
293
349
|
override func layoutSubviews() {
|
|
@@ -321,7 +377,6 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
321
377
|
).height
|
|
322
378
|
)
|
|
323
379
|
contentSize = CGSize(width: bounds.width, height: stack.frame.maxY + bottomInset)
|
|
324
|
-
refreshCaret()
|
|
325
380
|
onContentHeightMayChange?(contentSize.height)
|
|
326
381
|
}
|
|
327
382
|
|
|
@@ -351,19 +406,30 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
351
406
|
guard let data = updateJSON.data(using: .utf8),
|
|
352
407
|
let update = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
353
408
|
else { return }
|
|
409
|
+
let shouldPreserveTextFocus = textLeafViews.contains(where: \.isFirstResponder)
|
|
410
|
+
isApplyingEditorUpdate = true
|
|
411
|
+
defer { isApplyingEditorUpdate = false }
|
|
354
412
|
|
|
413
|
+
var textSelection: (UInt32, UInt32)?
|
|
355
414
|
if let selection = update["selection"] as? [String: Any] {
|
|
356
415
|
let type = selection["type"] as? String
|
|
357
416
|
selectedNodePos = type == "node" ? (selection["pos"] as? NSNumber)?.uint32Value : nil
|
|
358
417
|
let anchor = (selection["anchor"] as? NSNumber)?.uint32Value
|
|
359
418
|
let head = (selection["head"] as? NSNumber)?.uint32Value
|
|
360
419
|
selectedTextPos = type == "text" && anchor == head ? anchor : nil
|
|
420
|
+
if type == "text", let anchor, let head {
|
|
421
|
+
textSelection = (anchor, head)
|
|
422
|
+
}
|
|
361
423
|
refreshImageSelection()
|
|
362
|
-
refreshCaret()
|
|
363
424
|
}
|
|
364
425
|
guard let renderTree = update["renderTree"] as? [String: Any],
|
|
365
426
|
let root = renderTree["root"] as? [String: Any]
|
|
366
|
-
else {
|
|
427
|
+
else {
|
|
428
|
+
if let textSelection {
|
|
429
|
+
applyNativeSelection(anchor: textSelection.0, head: textSelection.1, preservingFocus: shouldPreserveTextFocus)
|
|
430
|
+
}
|
|
431
|
+
return
|
|
432
|
+
}
|
|
367
433
|
|
|
368
434
|
currentEditorId = editorId
|
|
369
435
|
isApplyingRemoteUpdate = true
|
|
@@ -375,10 +441,12 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
375
441
|
}
|
|
376
442
|
refreshTableCellRegistry()
|
|
377
443
|
refreshImageSelection()
|
|
378
|
-
refreshCaret()
|
|
379
444
|
currentRoot = root
|
|
380
445
|
refreshPlaceholder()
|
|
381
446
|
isApplyingRemoteUpdate = false
|
|
447
|
+
if let textSelection {
|
|
448
|
+
applyNativeSelection(anchor: textSelection.0, head: textSelection.1, preservingFocus: shouldPreserveTextFocus)
|
|
449
|
+
}
|
|
382
450
|
setNeedsLayout()
|
|
383
451
|
layoutIfNeeded()
|
|
384
452
|
}
|
|
@@ -486,6 +554,10 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
486
554
|
rebuildAll(from: root)
|
|
487
555
|
return
|
|
488
556
|
}
|
|
557
|
+
if let leaf = oldView as? TextLeafView {
|
|
558
|
+
configureTextLeaf(leaf, for: node)
|
|
559
|
+
return
|
|
560
|
+
}
|
|
489
561
|
guard let parent = oldView.superview else {
|
|
490
562
|
rebuildAll(from: root)
|
|
491
563
|
return
|
|
@@ -521,7 +593,7 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
521
593
|
let blockPos = (node["docPos"] as? NSNumber)?.uint32Value ?? 0
|
|
522
594
|
let blockSize = (node["docSize"] as? NSNumber)?.uint32Value ?? 0
|
|
523
595
|
leaf.positionSegments = [
|
|
524
|
-
.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),
|
|
525
597
|
]
|
|
526
598
|
} else {
|
|
527
599
|
leaf.positionSegments = segments
|
|
@@ -849,6 +921,7 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
849
921
|
cell.backgroundColor = cell.baseFillColor
|
|
850
922
|
cell.isUserInteractionEnabled = true
|
|
851
923
|
let tap = UITapGestureRecognizer(target: self, action: #selector(handleCellTap(_:)))
|
|
924
|
+
tap.delegate = self
|
|
852
925
|
cell.addGestureRecognizer(tap)
|
|
853
926
|
tableCells.append(cell)
|
|
854
927
|
for (index, child) in (node["children"] as? [[String: Any]] ?? []).enumerated() {
|
|
@@ -860,8 +933,25 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
860
933
|
return cell
|
|
861
934
|
}
|
|
862
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
|
+
|
|
863
946
|
private func textBlockView(for node: [String: Any], depth: Int) -> UIView {
|
|
864
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
|
|
865
955
|
leaf.font = font(for: node)
|
|
866
956
|
let nodeType = node["nodeType"] as? String ?? "paragraph"
|
|
867
957
|
let textStyle = currentTheme?.effectiveTextStyle(for: nodeType)
|
|
@@ -896,9 +986,10 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
896
986
|
.init(
|
|
897
987
|
range: NSRange(location: 0, length: 0),
|
|
898
988
|
text: "",
|
|
899
|
-
docPos: blockPos,
|
|
900
|
-
docSize:
|
|
901
|
-
isAtom: false
|
|
989
|
+
docPos: blockPos + (blockSize >= 2 ? 1 : 0),
|
|
990
|
+
docSize: 0,
|
|
991
|
+
isAtom: false,
|
|
992
|
+
nodeType: nil
|
|
902
993
|
),
|
|
903
994
|
]
|
|
904
995
|
}
|
|
@@ -917,16 +1008,17 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
917
1008
|
leaf.textContainerInset = .zero
|
|
918
1009
|
}
|
|
919
1010
|
leaf.isScrollEnabled = false
|
|
920
|
-
leaf.isEditable =
|
|
921
|
-
leaf.isSelectable =
|
|
922
|
-
leaf.textContainer.widthTracksTextView =
|
|
1011
|
+
leaf.isEditable = isEditable
|
|
1012
|
+
leaf.isSelectable = isEditable
|
|
1013
|
+
leaf.textContainer.widthTracksTextView = true
|
|
923
1014
|
leaf.textContainer.lineFragmentPadding = 0
|
|
924
|
-
leaf.
|
|
925
|
-
|
|
926
|
-
|
|
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)
|
|
927
1021
|
}
|
|
928
|
-
leaf.addGestureRecognizer(UITapGestureRecognizer(target: leaf, action: #selector(TextLeafView.activate(_:))))
|
|
929
|
-
return leaf
|
|
930
1022
|
}
|
|
931
1023
|
|
|
932
1024
|
private func attributedInlineContent(
|
|
@@ -953,7 +1045,8 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
953
1045
|
text: value,
|
|
954
1046
|
docPos: docPos,
|
|
955
1047
|
docSize: docSize,
|
|
956
|
-
isAtom: false
|
|
1048
|
+
isAtom: false,
|
|
1049
|
+
nodeType: nil
|
|
957
1050
|
))
|
|
958
1051
|
return
|
|
959
1052
|
}
|
|
@@ -977,7 +1070,8 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
977
1070
|
text: value,
|
|
978
1071
|
docPos: docPos,
|
|
979
1072
|
docSize: max(docSize, 1),
|
|
980
|
-
isAtom: true
|
|
1073
|
+
isAtom: true,
|
|
1074
|
+
nodeType: nodeType
|
|
981
1075
|
))
|
|
982
1076
|
return
|
|
983
1077
|
}
|
|
@@ -1187,16 +1281,132 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
1187
1281
|
}
|
|
1188
1282
|
}
|
|
1189
1283
|
|
|
1190
|
-
private func
|
|
1191
|
-
|
|
1192
|
-
|
|
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)
|
|
1193
1401
|
else {
|
|
1194
|
-
|
|
1402
|
+
applyUpdateJSON(editorGetCurrentState(id: currentEditorId), editorId: currentEditorId)
|
|
1195
1403
|
return
|
|
1196
1404
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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)
|
|
1200
1410
|
}
|
|
1201
1411
|
|
|
1202
1412
|
func firstImageGeometryForTesting() -> (docPos: UInt32, rect: CGRect)? {
|