@openeditor/react-native-prose-editor 0.0.9 → 0.0.11
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 +476 -53
- 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/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/Generated_editor_core.swift +15 -0
- package/ios/NativeBlockEditorSurface.swift +371 -60
- 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/ios/editor_coreFFI/editor_coreFFI.h +11 -0
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +31 -0
- 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
|
|
@@ -713,8 +785,6 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
713
785
|
row.alignment = .top
|
|
714
786
|
row.spacing = 8
|
|
715
787
|
|
|
716
|
-
row.addArrangedSubview(markerView(for: node, parentListType: parentListType, index: index))
|
|
717
|
-
|
|
718
788
|
let content = UIStackView()
|
|
719
789
|
content.axis = .vertical
|
|
720
790
|
content.spacing = 4
|
|
@@ -724,11 +794,20 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
724
794
|
if content.arrangedSubviews.isEmpty {
|
|
725
795
|
content.addArrangedSubview(textBlockView(for: node, depth: depth + 1))
|
|
726
796
|
}
|
|
797
|
+
if parentListType == "toggleList" || parentListType == "toggle_list" {
|
|
798
|
+
row.addArrangedSubview(toggleMarkerView(for: node, content: content))
|
|
799
|
+
setToggleContent(content, open: boolAttr(node, "open") ?? true)
|
|
800
|
+
} else {
|
|
801
|
+
row.addArrangedSubview(markerView(for: node, parentListType: parentListType, index: index))
|
|
802
|
+
}
|
|
727
803
|
row.addArrangedSubview(content)
|
|
728
804
|
return row
|
|
729
805
|
}
|
|
730
806
|
|
|
731
807
|
private func blockContainerView(for node: [String: Any], depth: Int, path: [Int]) -> UIView {
|
|
808
|
+
if (node["nodeType"] as? String) == "callout" {
|
|
809
|
+
return calloutView(for: node, depth: depth, path: path)
|
|
810
|
+
}
|
|
732
811
|
let container = UIStackView()
|
|
733
812
|
container.axis = .vertical
|
|
734
813
|
container.spacing = 4
|
|
@@ -762,6 +841,96 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
762
841
|
return container
|
|
763
842
|
}
|
|
764
843
|
|
|
844
|
+
private func calloutView(for node: [String: Any], depth: Int, path: [Int]) -> UIView {
|
|
845
|
+
let tone = stringAttr(node, "tone") ?? "info"
|
|
846
|
+
let accent: UIColor = switch tone {
|
|
847
|
+
case "success": .systemGreen
|
|
848
|
+
case "warning": .systemOrange
|
|
849
|
+
case "danger": .systemRed
|
|
850
|
+
default: .systemBlue
|
|
851
|
+
}
|
|
852
|
+
let container = UIStackView()
|
|
853
|
+
container.axis = .vertical
|
|
854
|
+
container.spacing = 8
|
|
855
|
+
container.layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
|
|
856
|
+
container.isLayoutMarginsRelativeArrangement = true
|
|
857
|
+
container.backgroundColor = accent.withAlphaComponent(0.1)
|
|
858
|
+
container.layer.borderColor = accent.withAlphaComponent(0.55).cgColor
|
|
859
|
+
container.layer.borderWidth = 1
|
|
860
|
+
container.layer.cornerRadius = 10
|
|
861
|
+
|
|
862
|
+
let toneButton = UIButton(type: .system)
|
|
863
|
+
toneButton.setTitle(tone.capitalized, for: .normal)
|
|
864
|
+
toneButton.setTitleColor(accent, for: .normal)
|
|
865
|
+
toneButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .semibold)
|
|
866
|
+
toneButton.contentHorizontalAlignment = .leading
|
|
867
|
+
toneButton.isEnabled = isEditable
|
|
868
|
+
toneButton.addAction(UIAction { [weak self] _ in
|
|
869
|
+
self?.cycleCalloutTone(node)
|
|
870
|
+
}, for: .touchUpInside)
|
|
871
|
+
container.addArrangedSubview(toneButton)
|
|
872
|
+
|
|
873
|
+
for (index, child) in (node["children"] as? [[String: Any]] ?? []).enumerated() {
|
|
874
|
+
container.addArrangedSubview(view(for: child, depth: depth + 1, path: path + [index]))
|
|
875
|
+
}
|
|
876
|
+
return container
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
private func toggleMarkerView(for node: [String: Any], content: UIStackView) -> UIView {
|
|
880
|
+
let open = boolAttr(node, "open") ?? true
|
|
881
|
+
let button = UIButton(type: .system)
|
|
882
|
+
button.setTitle(open ? "⌄" : "›", for: .normal)
|
|
883
|
+
button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
|
|
884
|
+
button.accessibilityLabel = open ? "Collapse toggle item" : "Expand toggle item"
|
|
885
|
+
button.addAction(UIAction { [weak self, weak button, weak content] _ in
|
|
886
|
+
guard let self, let button, let content else { return }
|
|
887
|
+
let nextOpen = button.title(for: .normal) != "⌄"
|
|
888
|
+
button.setTitle(nextOpen ? "⌄" : "›", for: .normal)
|
|
889
|
+
button.accessibilityLabel = nextOpen ? "Collapse toggle item" : "Expand toggle item"
|
|
890
|
+
self.setToggleContent(content, open: nextOpen)
|
|
891
|
+
if self.isEditable {
|
|
892
|
+
self.updateNodeAttrs(node, patch: ["open": nextOpen])
|
|
893
|
+
}
|
|
894
|
+
}, for: .touchUpInside)
|
|
895
|
+
let wrapper = UIView()
|
|
896
|
+
wrapper.widthAnchor.constraint(equalToConstant: 24).isActive = true
|
|
897
|
+
wrapper.heightAnchor.constraint(equalToConstant: 24).isActive = true
|
|
898
|
+
button.translatesAutoresizingMaskIntoConstraints = false
|
|
899
|
+
wrapper.addSubview(button)
|
|
900
|
+
NSLayoutConstraint.activate([
|
|
901
|
+
button.centerXAnchor.constraint(equalTo: wrapper.centerXAnchor),
|
|
902
|
+
button.centerYAnchor.constraint(equalTo: wrapper.centerYAnchor),
|
|
903
|
+
])
|
|
904
|
+
return wrapper
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private func setToggleContent(_ content: UIStackView, open: Bool) {
|
|
908
|
+
for (index, child) in content.arrangedSubviews.enumerated() where index > 0 {
|
|
909
|
+
child.isHidden = !open
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private func cycleCalloutTone(_ node: [String: Any]) {
|
|
914
|
+
guard isEditable else { return }
|
|
915
|
+
let tones = ["info", "success", "warning", "danger"]
|
|
916
|
+
let current = stringAttr(node, "tone") ?? "info"
|
|
917
|
+
let next = tones[((tones.firstIndex(of: current) ?? 0) + 1) % tones.count]
|
|
918
|
+
updateNodeAttrs(node, patch: ["tone": next])
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private func updateNodeAttrs(_ node: [String: Any], patch: [String: Any]) {
|
|
922
|
+
guard currentEditorId != 0 else { return }
|
|
923
|
+
var attrs = node["attrs"] as? [String: Any] ?? [:]
|
|
924
|
+
attrs.merge(patch) { _, next in next }
|
|
925
|
+
guard let data = try? JSONSerialization.data(withJSONObject: attrs),
|
|
926
|
+
let json = String(data: data, encoding: .utf8)
|
|
927
|
+
else { return }
|
|
928
|
+
let pos = (node["docPos"] as? NSNumber)?.uint32Value ?? 0
|
|
929
|
+
let updateJSON = editorUpdateNodeAttrs(id: currentEditorId, pos: pos, attrsJson: json)
|
|
930
|
+
applyUpdateJSON(updateJSON, editorId: currentEditorId)
|
|
931
|
+
onAppliedUpdate?(updateJSON)
|
|
932
|
+
}
|
|
933
|
+
|
|
765
934
|
func blockquoteStripeRectsForTesting() -> [CGRect] {
|
|
766
935
|
layoutIfNeeded()
|
|
767
936
|
return descendants(of: self)
|
|
@@ -849,6 +1018,7 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
849
1018
|
cell.backgroundColor = cell.baseFillColor
|
|
850
1019
|
cell.isUserInteractionEnabled = true
|
|
851
1020
|
let tap = UITapGestureRecognizer(target: self, action: #selector(handleCellTap(_:)))
|
|
1021
|
+
tap.delegate = self
|
|
852
1022
|
cell.addGestureRecognizer(tap)
|
|
853
1023
|
tableCells.append(cell)
|
|
854
1024
|
for (index, child) in (node["children"] as? [[String: Any]] ?? []).enumerated() {
|
|
@@ -860,8 +1030,25 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
860
1030
|
return cell
|
|
861
1031
|
}
|
|
862
1032
|
|
|
1033
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
1034
|
+
guard gestureRecognizer.view is CellView else { return true }
|
|
1035
|
+
var touchedView: UIView? = touch.view
|
|
1036
|
+
while let view = touchedView, view !== gestureRecognizer.view {
|
|
1037
|
+
if view is UITextView || view is UIControl { return false }
|
|
1038
|
+
touchedView = view.superview
|
|
1039
|
+
}
|
|
1040
|
+
return true
|
|
1041
|
+
}
|
|
1042
|
+
|
|
863
1043
|
private func textBlockView(for node: [String: Any], depth: Int) -> UIView {
|
|
864
1044
|
let leaf = TextLeafView()
|
|
1045
|
+
configureTextLeaf(leaf, for: node)
|
|
1046
|
+
return leaf
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private func configureTextLeaf(_ leaf: TextLeafView, for node: [String: Any]) {
|
|
1050
|
+
let wasFirstResponder = leaf.isFirstResponder
|
|
1051
|
+
let previousSelection = leaf.selectedRange
|
|
865
1052
|
leaf.font = font(for: node)
|
|
866
1053
|
let nodeType = node["nodeType"] as? String ?? "paragraph"
|
|
867
1054
|
let textStyle = currentTheme?.effectiveTextStyle(for: nodeType)
|
|
@@ -896,9 +1083,10 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
896
1083
|
.init(
|
|
897
1084
|
range: NSRange(location: 0, length: 0),
|
|
898
1085
|
text: "",
|
|
899
|
-
docPos: blockPos,
|
|
900
|
-
docSize:
|
|
901
|
-
isAtom: false
|
|
1086
|
+
docPos: blockPos + (blockSize >= 2 ? 1 : 0),
|
|
1087
|
+
docSize: 0,
|
|
1088
|
+
isAtom: false,
|
|
1089
|
+
nodeType: nil
|
|
902
1090
|
),
|
|
903
1091
|
]
|
|
904
1092
|
}
|
|
@@ -917,16 +1105,17 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
917
1105
|
leaf.textContainerInset = .zero
|
|
918
1106
|
}
|
|
919
1107
|
leaf.isScrollEnabled = false
|
|
920
|
-
leaf.isEditable =
|
|
921
|
-
leaf.isSelectable =
|
|
922
|
-
leaf.textContainer.widthTracksTextView =
|
|
1108
|
+
leaf.isEditable = isEditable
|
|
1109
|
+
leaf.isSelectable = isEditable
|
|
1110
|
+
leaf.textContainer.widthTracksTextView = true
|
|
923
1111
|
leaf.textContainer.lineFragmentPadding = 0
|
|
924
|
-
leaf.
|
|
925
|
-
|
|
926
|
-
|
|
1112
|
+
leaf.delegate = self
|
|
1113
|
+
applyInputConfiguration(to: leaf)
|
|
1114
|
+
if wasFirstResponder {
|
|
1115
|
+
let location = min(previousSelection.location, leaf.textStorage.length)
|
|
1116
|
+
let length = min(previousSelection.length, leaf.textStorage.length - location)
|
|
1117
|
+
leaf.selectedRange = NSRange(location: location, length: length)
|
|
927
1118
|
}
|
|
928
|
-
leaf.addGestureRecognizer(UITapGestureRecognizer(target: leaf, action: #selector(TextLeafView.activate(_:))))
|
|
929
|
-
return leaf
|
|
930
1119
|
}
|
|
931
1120
|
|
|
932
1121
|
private func attributedInlineContent(
|
|
@@ -953,7 +1142,8 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
953
1142
|
text: value,
|
|
954
1143
|
docPos: docPos,
|
|
955
1144
|
docSize: docSize,
|
|
956
|
-
isAtom: false
|
|
1145
|
+
isAtom: false,
|
|
1146
|
+
nodeType: nil
|
|
957
1147
|
))
|
|
958
1148
|
return
|
|
959
1149
|
}
|
|
@@ -977,7 +1167,8 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
977
1167
|
text: value,
|
|
978
1168
|
docPos: docPos,
|
|
979
1169
|
docSize: max(docSize, 1),
|
|
980
|
-
isAtom: true
|
|
1170
|
+
isAtom: true,
|
|
1171
|
+
nodeType: nodeType
|
|
981
1172
|
))
|
|
982
1173
|
return
|
|
983
1174
|
}
|
|
@@ -1187,16 +1378,132 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
1187
1378
|
}
|
|
1188
1379
|
}
|
|
1189
1380
|
|
|
1190
|
-
private func
|
|
1191
|
-
|
|
1192
|
-
|
|
1381
|
+
private func applyNativeSelection(anchor: UInt32, head: UInt32, preservingFocus: Bool) {
|
|
1382
|
+
let candidates = textLeafViews.filter {
|
|
1383
|
+
$0.nativeOffset(forDocPosition: anchor) != nil &&
|
|
1384
|
+
$0.nativeOffset(forDocPosition: head) != nil
|
|
1385
|
+
}
|
|
1386
|
+
guard let leaf = candidates.first(where: \.isFirstResponder)
|
|
1387
|
+
?? (anchor == head ? candidates.last : candidates.first),
|
|
1388
|
+
let anchorOffset = leaf.nativeOffset(forDocPosition: anchor),
|
|
1389
|
+
let headOffset = leaf.nativeOffset(forDocPosition: head),
|
|
1390
|
+
let start = leaf.position(
|
|
1391
|
+
from: leaf.beginningOfDocument,
|
|
1392
|
+
offset: min(anchorOffset, headOffset)
|
|
1393
|
+
),
|
|
1394
|
+
let end = leaf.position(
|
|
1395
|
+
from: leaf.beginningOfDocument,
|
|
1396
|
+
offset: max(anchorOffset, headOffset)
|
|
1397
|
+
),
|
|
1398
|
+
let range = leaf.textRange(from: start, to: end)
|
|
1399
|
+
else { return }
|
|
1400
|
+
leaf.selectedTextRange = range
|
|
1401
|
+
if preservingFocus, !leaf.isFirstResponder {
|
|
1402
|
+
leaf.becomeFirstResponder()
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
func textViewDidChangeSelection(_ textView: UITextView) {
|
|
1407
|
+
guard !isApplyingEditorUpdate, isEditable, currentEditorId != 0,
|
|
1408
|
+
let leaf = textView as? TextLeafView,
|
|
1409
|
+
let range = leaf.selectedTextRange
|
|
1410
|
+
else { return }
|
|
1411
|
+
let anchorOffset = leaf.offset(from: leaf.beginningOfDocument, to: range.start)
|
|
1412
|
+
let headOffset = leaf.offset(from: leaf.beginningOfDocument, to: range.end)
|
|
1413
|
+
guard let anchor = leaf.docPosition(forNativeOffset: anchorOffset),
|
|
1414
|
+
let head = leaf.docPosition(forNativeOffset: headOffset)
|
|
1415
|
+
else { return }
|
|
1416
|
+
editorSetSelection(id: currentEditorId, anchor: anchor, head: head)
|
|
1417
|
+
onAppliedUpdate?(editorGetSelectionState(id: currentEditorId))
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
func textViewDidChange(_ textView: UITextView) {
|
|
1421
|
+
guard !isApplyingEditorUpdate, isEditable, currentEditorId != 0,
|
|
1422
|
+
let leaf = textView as? TextLeafView,
|
|
1423
|
+
textView.markedTextRange == nil,
|
|
1424
|
+
let baseline = leaf.compositionBaselineText
|
|
1425
|
+
else { return }
|
|
1426
|
+
leaf.compositionBaselineText = nil
|
|
1427
|
+
commitNativeText(in: leaf, baseline: baseline, current: leaf.text ?? "")
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
func textViewDidBeginEditing(_ textView: UITextView) {
|
|
1431
|
+
onFocusChange?(true)
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
func textViewDidEndEditing(_ textView: UITextView) {
|
|
1435
|
+
if !textLeafViews.contains(where: { $0.isFirstResponder }) {
|
|
1436
|
+
onFocusChange?(false)
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
func textView(
|
|
1441
|
+
_ textView: UITextView,
|
|
1442
|
+
shouldChangeTextIn range: NSRange,
|
|
1443
|
+
replacementText text: String
|
|
1444
|
+
) -> Bool {
|
|
1445
|
+
guard !isApplyingEditorUpdate, isEditable, currentEditorId != 0,
|
|
1446
|
+
let leaf = textView as? TextLeafView
|
|
1447
|
+
else { return false }
|
|
1448
|
+
|
|
1449
|
+
if text.isEmpty, range.length == 0, range.location == 0,
|
|
1450
|
+
let docPosition = leaf.docPosition(forNativeOffset: 0) {
|
|
1451
|
+
let scalar = editorDocToScalar(id: currentEditorId, docPos: docPosition)
|
|
1452
|
+
let update = editorDeleteBackwardAtSelectionScalar(
|
|
1453
|
+
id: currentEditorId,
|
|
1454
|
+
scalarAnchor: scalar,
|
|
1455
|
+
scalarHead: scalar
|
|
1456
|
+
)
|
|
1457
|
+
applyUpdateJSON(update, editorId: currentEditorId)
|
|
1458
|
+
onAppliedUpdate?(update)
|
|
1459
|
+
return false
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if text == "\n" {
|
|
1463
|
+
guard let anchor = leaf.docPosition(forNativeOffset: range.location),
|
|
1464
|
+
let head = leaf.docPosition(forNativeOffset: NSMaxRange(range))
|
|
1465
|
+
else { return false }
|
|
1466
|
+
editorSetSelection(id: currentEditorId, anchor: anchor, head: head)
|
|
1467
|
+
if anchor != head {
|
|
1468
|
+
_ = editorReplaceSelectionText(id: currentEditorId, text: "")
|
|
1469
|
+
}
|
|
1470
|
+
let update = editorSplitBlock(id: currentEditorId, pos: anchor)
|
|
1471
|
+
applyUpdateJSON(update, editorId: currentEditorId)
|
|
1472
|
+
onAppliedUpdate?(update)
|
|
1473
|
+
return false
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
if leaf.compositionBaselineText == nil {
|
|
1477
|
+
leaf.compositionBaselineText = leaf.text ?? ""
|
|
1478
|
+
}
|
|
1479
|
+
return true
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
private func commitNativeText(in leaf: TextLeafView, baseline: String, current: String) {
|
|
1483
|
+
guard baseline != current else { return }
|
|
1484
|
+
let old = baseline as NSString
|
|
1485
|
+
let new = current as NSString
|
|
1486
|
+
var prefix = 0
|
|
1487
|
+
while prefix < old.length, prefix < new.length,
|
|
1488
|
+
old.character(at: prefix) == new.character(at: prefix) { prefix += 1 }
|
|
1489
|
+
var oldSuffix = old.length
|
|
1490
|
+
var newSuffix = new.length
|
|
1491
|
+
while oldSuffix > prefix, newSuffix > prefix,
|
|
1492
|
+
old.character(at: oldSuffix - 1) == new.character(at: newSuffix - 1) {
|
|
1493
|
+
oldSuffix -= 1
|
|
1494
|
+
newSuffix -= 1
|
|
1495
|
+
}
|
|
1496
|
+
guard let anchor = leaf.docPosition(forNativeOffset: prefix),
|
|
1497
|
+
let head = leaf.docPosition(forNativeOffset: oldSuffix)
|
|
1193
1498
|
else {
|
|
1194
|
-
|
|
1499
|
+
applyUpdateJSON(editorGetCurrentState(id: currentEditorId), editorId: currentEditorId)
|
|
1195
1500
|
return
|
|
1196
1501
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1502
|
+
let replacement = new.substring(with: NSRange(location: prefix, length: newSuffix - prefix))
|
|
1503
|
+
editorSetSelection(id: currentEditorId, anchor: anchor, head: head)
|
|
1504
|
+
let update = editorReplaceSelectionText(id: currentEditorId, text: replacement)
|
|
1505
|
+
applyUpdateJSON(update, editorId: currentEditorId)
|
|
1506
|
+
onAppliedUpdate?(update)
|
|
1200
1507
|
}
|
|
1201
1508
|
|
|
1202
1509
|
func firstImageGeometryForTesting() -> (docPos: UInt32, rect: CGRect)? {
|
|
@@ -1339,6 +1646,10 @@ final class NativeBlockEditorSurface: UIScrollView {
|
|
|
1339
1646
|
return attrs[key] as? Bool
|
|
1340
1647
|
}
|
|
1341
1648
|
|
|
1649
|
+
private func stringAttr(_ node: [String: Any], _ key: String) -> String? {
|
|
1650
|
+
(node["attrs"] as? [String: Any])?[key] as? String
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1342
1653
|
private func isCodeBlock(_ node: [String: Any]) -> Bool {
|
|
1343
1654
|
let nodeType = node["nodeType"] as? String
|
|
1344
1655
|
return nodeType == "codeBlock" || nodeType == "code_block"
|