@apollohg/react-native-prose-editor 0.5.8 → 0.5.9
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/android/src/main/java/com/apollohg/editor/EditorTheme.kt +4 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +4 -1
- 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/NativeEditorExpoView.swift +63 -3
- package/ios/RichTextEditorView.swift +33 -4
- 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
|
@@ -362,6 +362,10 @@ private fun parseColor(raw: String?): Int? {
|
|
|
362
362
|
val value = raw?.trim()?.lowercase() ?: return null
|
|
363
363
|
if (value.isEmpty()) return null
|
|
364
364
|
|
|
365
|
+
when (value) {
|
|
366
|
+
"clear", "transparent" -> return Color.TRANSPARENT
|
|
367
|
+
}
|
|
368
|
+
|
|
365
369
|
parseCssHexColor(value)?.let { return it }
|
|
366
370
|
|
|
367
371
|
try {
|
|
@@ -54,6 +54,7 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
54
54
|
private var baseBackgroundColor: Int = Color.WHITE
|
|
55
55
|
private var viewportBottomInsetPx: Int = 0
|
|
56
56
|
internal var appliedCornerRadiusPx: Float = 0f
|
|
57
|
+
internal var appliedBackgroundColorForTesting: Int = Color.WHITE
|
|
57
58
|
|
|
58
59
|
/** Binds or unbinds the Rust editor instance. */
|
|
59
60
|
var editorId: Long = 0
|
|
@@ -251,13 +252,15 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
251
252
|
|
|
252
253
|
private fun updateScrollContainerAppearance() {
|
|
253
254
|
val cornerRadiusPx = (theme?.borderRadius ?: 0f) * resources.displayMetrics.density
|
|
255
|
+
val backgroundColor = theme?.backgroundColor ?: baseBackgroundColor
|
|
254
256
|
editorViewport.background = GradientDrawable().apply {
|
|
255
257
|
cornerRadius = cornerRadiusPx
|
|
256
|
-
setColor(
|
|
258
|
+
setColor(backgroundColor)
|
|
257
259
|
}
|
|
258
260
|
editorViewport.clipToOutline = cornerRadiusPx > 0f
|
|
259
261
|
editorScrollView.setBackgroundColor(Color.TRANSPARENT)
|
|
260
262
|
appliedCornerRadiusPx = cornerRadiusPx
|
|
263
|
+
appliedBackgroundColorForTesting = backgroundColor
|
|
261
264
|
}
|
|
262
265
|
|
|
263
266
|
private fun updateScrollContainerInsets() {
|
|
Binary file
|
|
Binary file
|
|
@@ -1576,6 +1576,46 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
1576
1576
|
}
|
|
1577
1577
|
}
|
|
1578
1578
|
|
|
1579
|
+
/// Keeps iOS keyboard integrations on the inputAccessoryView path when the
|
|
1580
|
+
/// visible toolbar is rendered outside the native keyboard accessory.
|
|
1581
|
+
final class EditorAccessoryPlaceholderView: UIView {
|
|
1582
|
+
override init(frame: CGRect) {
|
|
1583
|
+
super.init(
|
|
1584
|
+
frame: CGRect(
|
|
1585
|
+
x: frame.origin.x,
|
|
1586
|
+
y: frame.origin.y,
|
|
1587
|
+
width: frame.width,
|
|
1588
|
+
height: 0
|
|
1589
|
+
)
|
|
1590
|
+
)
|
|
1591
|
+
commonInit()
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
required init?(coder: NSCoder) {
|
|
1595
|
+
return nil
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
override var intrinsicContentSize: CGSize {
|
|
1599
|
+
CGSize(width: UIView.noIntrinsicMetric, height: 0)
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
|
1603
|
+
CGSize(width: size.width, height: 0)
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
1607
|
+
false
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
private func commonInit() {
|
|
1611
|
+
frame.size.height = 0
|
|
1612
|
+
backgroundColor = .clear
|
|
1613
|
+
isOpaque = false
|
|
1614
|
+
isUserInteractionEnabled = false
|
|
1615
|
+
autoresizingMask = [.flexibleWidth]
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1579
1619
|
class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognizerDelegate {
|
|
1580
1620
|
|
|
1581
1621
|
// MARK: - Subviews
|
|
@@ -1585,6 +1625,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1585
1625
|
frame: .zero,
|
|
1586
1626
|
inputViewStyle: .keyboard
|
|
1587
1627
|
)
|
|
1628
|
+
private let accessoryPlaceholder = EditorAccessoryPlaceholderView(frame: .zero)
|
|
1588
1629
|
private var toolbarFrameInWindow: CGRect?
|
|
1589
1630
|
private var didApplyAutoFocus = false
|
|
1590
1631
|
private var toolbarState = NativeToolbarState.empty
|
|
@@ -2269,14 +2310,33 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2269
2310
|
func triggerMentionSuggestionTapForTesting(at index: Int) {
|
|
2270
2311
|
accessoryToolbar.triggerMentionSuggestionTapForTesting(at: index)
|
|
2271
2312
|
}
|
|
2313
|
+
|
|
2314
|
+
func inputAccessoryViewForTesting() -> UIView? {
|
|
2315
|
+
richTextView.textView.inputAccessoryView
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
func isUsingAccessoryToolbarForTesting() -> Bool {
|
|
2319
|
+
richTextView.textView.inputAccessoryView === accessoryToolbar
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
func isUsingAccessoryPlaceholderForTesting() -> Bool {
|
|
2323
|
+
richTextView.textView.inputAccessoryView === accessoryPlaceholder
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2272
2326
|
private func updateAccessoryToolbarVisibility() {
|
|
2273
2327
|
refreshSystemAssistantToolbarIfNeeded()
|
|
2274
|
-
let nextAccessoryView: UIView?
|
|
2328
|
+
let nextAccessoryView: UIView?
|
|
2329
|
+
if showsToolbar &&
|
|
2275
2330
|
toolbarPlacement == "keyboard" &&
|
|
2276
2331
|
richTextView.textView.isEditable &&
|
|
2277
2332
|
!shouldUseSystemAssistantToolbar
|
|
2278
|
-
|
|
2279
|
-
|
|
2333
|
+
{
|
|
2334
|
+
nextAccessoryView = accessoryToolbar
|
|
2335
|
+
} else if richTextView.textView.isEditable && !shouldUseSystemAssistantToolbar {
|
|
2336
|
+
nextAccessoryView = accessoryPlaceholder
|
|
2337
|
+
} else {
|
|
2338
|
+
nextAccessoryView = nil
|
|
2339
|
+
}
|
|
2280
2340
|
if richTextView.textView.inputAccessoryView !== nextAccessoryView {
|
|
2281
2341
|
richTextView.textView.inputAccessoryView = nextAccessoryView
|
|
2282
2342
|
if richTextView.textView.isFirstResponder {
|
|
@@ -1050,7 +1050,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1050
1050
|
// Register as the text storage delegate so we can detect unauthorized
|
|
1051
1051
|
// mutations (reconciliation fallback).
|
|
1052
1052
|
textStorage.delegate = self
|
|
1053
|
-
|
|
1053
|
+
ensureInternalTextViewDelegate()
|
|
1054
1054
|
addGestureRecognizer(imageSelectionTapRecognizer)
|
|
1055
1055
|
installImageSelectionTapDependencies()
|
|
1056
1056
|
|
|
@@ -1161,6 +1161,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1161
1161
|
override func becomeFirstResponder() -> Bool {
|
|
1162
1162
|
let didBecomeFirstResponder = super.becomeFirstResponder()
|
|
1163
1163
|
if didBecomeFirstResponder {
|
|
1164
|
+
ensureInternalTextViewDelegate()
|
|
1165
|
+
DispatchQueue.main.async { [weak self] in
|
|
1166
|
+
self?.ensureInternalTextViewDelegate()
|
|
1167
|
+
}
|
|
1164
1168
|
_ = normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded()
|
|
1165
1169
|
refreshTypingAttributesForSelection()
|
|
1166
1170
|
}
|
|
@@ -1340,6 +1344,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1340
1344
|
lastApplyUpdateTraceForTesting
|
|
1341
1345
|
}
|
|
1342
1346
|
|
|
1347
|
+
func isUsingInternalTextViewDelegateForTesting() -> Bool {
|
|
1348
|
+
(delegate as AnyObject?) === (self as AnyObject)
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1343
1351
|
func blockquoteStripeRectsForTesting() -> [CGRect] {
|
|
1344
1352
|
editorLayoutManager.blockquoteStripeRectsForTesting(in: textStorage)
|
|
1345
1353
|
}
|
|
@@ -1533,6 +1541,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1533
1541
|
/// - id: The editor ID from `editor_create()`.
|
|
1534
1542
|
/// - initialHTML: Optional HTML to set as initial content.
|
|
1535
1543
|
func bindEditor(id: UInt64, initialHTML: String? = nil) {
|
|
1544
|
+
ensureInternalTextViewDelegate()
|
|
1536
1545
|
editorId = id
|
|
1537
1546
|
|
|
1538
1547
|
if let html = initialHTML, !html.isEmpty {
|
|
@@ -1561,6 +1570,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1561
1570
|
/// Instead of calling `super.insertText()` (which would modify the
|
|
1562
1571
|
/// underlying text storage directly), we route through Rust.
|
|
1563
1572
|
override func insertText(_ text: String) {
|
|
1573
|
+
ensureInternalTextViewDelegate()
|
|
1564
1574
|
guard !isApplyingRustState else {
|
|
1565
1575
|
super.insertText(text)
|
|
1566
1576
|
return
|
|
@@ -1644,6 +1654,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1644
1654
|
/// If there's a range selection, delete the range. If it's a cursor,
|
|
1645
1655
|
/// delete the character (grapheme cluster) before the cursor.
|
|
1646
1656
|
override func deleteBackward() {
|
|
1657
|
+
ensureInternalTextViewDelegate()
|
|
1647
1658
|
guard !isApplyingRustState else {
|
|
1648
1659
|
super.deleteBackward()
|
|
1649
1660
|
return
|
|
@@ -1837,6 +1848,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1837
1848
|
///
|
|
1838
1849
|
/// We route the replacement through Rust to keep the document model in sync.
|
|
1839
1850
|
override func replace(_ range: UITextRange, withText text: String) {
|
|
1851
|
+
ensureInternalTextViewDelegate()
|
|
1840
1852
|
guard !isApplyingRustState else {
|
|
1841
1853
|
super.replace(range, withText: text)
|
|
1842
1854
|
return
|
|
@@ -1871,6 +1883,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1871
1883
|
/// UITextView display the composing text normally (with its underline
|
|
1872
1884
|
/// decoration). The text is NOT sent to Rust during composition.
|
|
1873
1885
|
override func setMarkedText(_ markedText: String?, selectedRange: NSRange) {
|
|
1886
|
+
ensureInternalTextViewDelegate()
|
|
1874
1887
|
isComposing = true
|
|
1875
1888
|
Self.inputLog.debug(
|
|
1876
1889
|
"[setMarkedText] marked=\(self.preview(markedText ?? ""), privacy: .public) nsRange=\(selectedRange.location),\(selectedRange.length) selection=\(self.selectionSummary(), privacy: .public)"
|
|
@@ -1886,6 +1899,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1886
1899
|
/// replace the marked text with the final text in the text storage,
|
|
1887
1900
|
/// but we intercept at a higher level.
|
|
1888
1901
|
override func unmarkText() {
|
|
1902
|
+
ensureInternalTextViewDelegate()
|
|
1889
1903
|
// Capture the finalized composed text before UIKit clears it.
|
|
1890
1904
|
composedText = markedTextRange.flatMap { text(in: $0) }
|
|
1891
1905
|
|
|
@@ -1919,6 +1933,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1919
1933
|
/// Attempts to extract HTML from the pasteboard first (for rich text paste),
|
|
1920
1934
|
/// falling back to plain text.
|
|
1921
1935
|
override func paste(_ sender: Any?) {
|
|
1936
|
+
ensureInternalTextViewDelegate()
|
|
1922
1937
|
guard editorId != 0 else {
|
|
1923
1938
|
super.paste(sender)
|
|
1924
1939
|
return
|
|
@@ -1977,6 +1992,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1977
1992
|
/// internally during tap handling and word-boundary resolution.
|
|
1978
1993
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
|
1979
1994
|
guard textView === self else { return }
|
|
1995
|
+
ensureInternalTextViewDelegate()
|
|
1980
1996
|
guard !isApplyingRustState else { return }
|
|
1981
1997
|
if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
|
|
1982
1998
|
return
|
|
@@ -2029,6 +2045,14 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2029
2045
|
interceptedInputDepth > 0
|
|
2030
2046
|
}
|
|
2031
2047
|
|
|
2048
|
+
private func ensureInternalTextViewDelegate() {
|
|
2049
|
+
// Some keyboard integrations replace UITextView's private delegate ivar
|
|
2050
|
+
// directly. The editor must own delegate callbacks so external observers
|
|
2051
|
+
// cannot inspect transient TextKit state during Rust-driven edits.
|
|
2052
|
+
guard (delegate as AnyObject?) !== (self as AnyObject) else { return }
|
|
2053
|
+
delegate = self
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2032
2056
|
private func performInterceptedInput(_ action: () -> Void) {
|
|
2033
2057
|
interceptedInputDepth += 1
|
|
2034
2058
|
Self.inputLog.debug(
|
|
@@ -3236,12 +3260,11 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3236
3260
|
var stringMutationNanos: UInt64 = 0
|
|
3237
3261
|
var attributeMutationNanos: UInt64 = 0
|
|
3238
3262
|
let previousTextStorageDelegate = textStorage.delegate
|
|
3239
|
-
let previousTextViewDelegate = delegate
|
|
3240
3263
|
textStorage.delegate = nil
|
|
3241
3264
|
delegate = nil
|
|
3242
3265
|
defer {
|
|
3243
3266
|
textStorage.delegate = previousTextStorageDelegate
|
|
3244
|
-
|
|
3267
|
+
ensureInternalTextViewDelegate()
|
|
3245
3268
|
}
|
|
3246
3269
|
if let replaceRange {
|
|
3247
3270
|
if shouldUseSmallPatchTextMutation {
|
|
@@ -3810,6 +3833,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3810
3833
|
///
|
|
3811
3834
|
/// - Parameter updateJSON: The JSON string from editor_insert_text, etc.
|
|
3812
3835
|
func applyUpdateJSON(_ updateJSON: String, notifyDelegate: Bool = true) {
|
|
3836
|
+
ensureInternalTextViewDelegate()
|
|
3813
3837
|
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3814
3838
|
let parseStartedAt = totalStartedAt
|
|
3815
3839
|
guard let data = updateJSON.data(using: .utf8),
|
|
@@ -3996,6 +4020,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3996
4020
|
/// Used for initial content loading (set_html / set_json return render
|
|
3997
4021
|
/// elements directly, not wrapped in an EditorUpdate).
|
|
3998
4022
|
func applyRenderJSON(_ renderJSON: String) {
|
|
4023
|
+
ensureInternalTextViewDelegate()
|
|
3999
4024
|
Self.updateLog.debug(
|
|
4000
4025
|
"[applyRenderJSON.begin] before=\(self.textSnapshotSummary(), privacy: .public)"
|
|
4001
4026
|
)
|
|
@@ -4031,7 +4056,11 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4031
4056
|
|
|
4032
4057
|
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4033
4058
|
isApplyingRustState = true
|
|
4034
|
-
|
|
4059
|
+
delegate = nil
|
|
4060
|
+
defer {
|
|
4061
|
+
ensureInternalTextViewDelegate()
|
|
4062
|
+
isApplyingRustState = false
|
|
4063
|
+
}
|
|
4035
4064
|
|
|
4036
4065
|
switch type {
|
|
4037
4066
|
case "text":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apollohg/react-native-prose-editor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Native rich text editor with Rust core for React Native",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/apollohg/react-native-prose-editor",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|