@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.
- 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 +323 -61
- package/ios/NativeEditorExpoView.swift +114 -117
- package/ios/NativeEditorModule.swift +1 -1
- package/ios/NativeInputSupport.swift +0 -302
- package/ios/PositionBridge.swift +22 -548
- package/ios/RichTextEditorView.swift +229 -4664
- 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? {
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
addSubview(
|
|
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:
|
|
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 {
|
|
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:
|
|
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 =
|
|
869
|
-
leaf.isSelectable =
|
|
870
|
-
leaf.textContainer.widthTracksTextView =
|
|
1011
|
+
leaf.isEditable = isEditable
|
|
1012
|
+
leaf.isSelectable = isEditable
|
|
1013
|
+
leaf.textContainer.widthTracksTextView = true
|
|
871
1014
|
leaf.textContainer.lineFragmentPadding = 0
|
|
872
|
-
leaf.
|
|
873
|
-
|
|
874
|
-
|
|
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
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
-
|
|
1402
|
+
applyUpdateJSON(editorGetCurrentState(id: currentEditorId), editorId: currentEditorId)
|
|
1143
1403
|
return
|
|
1144
1404
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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)? {
|