@apollohg/react-native-prose-editor 0.5.9 → 0.5.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/android/src/main/java/com/apollohg/editor/EditorEditText.kt +71 -36
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +103 -14
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +150 -23
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +4 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +2 -1
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +10 -0
- package/dist/EditorToolbar.d.ts +18 -1
- package/dist/EditorToolbar.js +156 -4
- package/dist/NativeRichTextEditor.d.ts +16 -0
- package/dist/NativeRichTextEditor.js +87 -11
- package/dist/index.d.ts +1 -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 +70 -8
- package/ios/NativeEditorModule.swift +3 -0
- package/ios/RichTextEditorView.swift +210 -31
- 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
|
@@ -704,7 +704,8 @@ private final class ImageResizeOverlayView: UIView {
|
|
|
704
704
|
/// For CJK input methods, `setMarkedText` / `unmarkText` are used. During
|
|
705
705
|
/// composition (marked text), we let UITextView handle it normally so the
|
|
706
706
|
/// user sees their composing text. When composition finalizes (`unmarkText`),
|
|
707
|
-
/// we
|
|
707
|
+
/// we commit the final text through Rust at the original Rust-authorized
|
|
708
|
+
/// replacement range.
|
|
708
709
|
///
|
|
709
710
|
/// ## Thread Safety
|
|
710
711
|
///
|
|
@@ -970,9 +971,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
970
971
|
private var pendingDeferredImageSelectionRange: NSRange?
|
|
971
972
|
private var pendingDeferredImageSelectionGeneration: UInt64 = 0
|
|
972
973
|
|
|
973
|
-
/// Stores the
|
|
974
|
-
///
|
|
975
|
-
|
|
974
|
+
/// Stores the Rust-authorized scalar range replaced by the active marked
|
|
975
|
+
/// text session. UIKit mutates visible TextKit state during composition,
|
|
976
|
+
/// so final commits must not infer their range from the transient cursor.
|
|
977
|
+
private var markedTextReplacementScalarRange: (from: UInt32, to: UInt32)?
|
|
978
|
+
private var markedTextReplacementUtf16Range: NSRange?
|
|
979
|
+
private var markedTextCompositionText: String?
|
|
976
980
|
|
|
977
981
|
private let editorLayoutManager: EditorLayoutManager
|
|
978
982
|
|
|
@@ -1580,6 +1584,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1580
1584
|
return
|
|
1581
1585
|
}
|
|
1582
1586
|
|
|
1587
|
+
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1588
|
+
let replacementRange = trackedMarkedTextReplacementRange()
|
|
1589
|
+
finishTransientMarkedTextMutation()
|
|
1590
|
+
commitMarkedText(text, replacementRange: replacementRange)
|
|
1591
|
+
return
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1583
1594
|
// Handle Enter/Return as a block split operation.
|
|
1584
1595
|
if text == "\n" {
|
|
1585
1596
|
performInterceptedInput {
|
|
@@ -1664,6 +1675,15 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1664
1675
|
return
|
|
1665
1676
|
}
|
|
1666
1677
|
|
|
1678
|
+
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1679
|
+
performTransientTextMutation {
|
|
1680
|
+
super.deleteBackward()
|
|
1681
|
+
}
|
|
1682
|
+
refreshMarkedTextCompositionText()
|
|
1683
|
+
isComposing = markedTextRange != nil || markedTextReplacementScalarRange != nil
|
|
1684
|
+
return
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1667
1687
|
guard let selectedRange = selectedTextRange else { return }
|
|
1668
1688
|
Self.inputLog.debug(
|
|
1669
1689
|
"[deleteBackward] selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
@@ -1858,6 +1878,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1858
1878
|
return
|
|
1859
1879
|
}
|
|
1860
1880
|
|
|
1881
|
+
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1882
|
+
let replacementRange = trackedMarkedTextReplacementRange()
|
|
1883
|
+
finishTransientMarkedTextMutation()
|
|
1884
|
+
commitMarkedText(text, replacementRange: replacementRange)
|
|
1885
|
+
return
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1861
1888
|
let scalarRange = PositionBridge.textRangeToScalarRange(range, in: self)
|
|
1862
1889
|
Self.inputLog.debug(
|
|
1863
1890
|
"[replace] text=\(self.preview(text), privacy: .public) scalarRange=\(scalarRange.from)-\(scalarRange.to) selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
@@ -1884,46 +1911,178 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1884
1911
|
/// decoration). The text is NOT sent to Rust during composition.
|
|
1885
1912
|
override func setMarkedText(_ markedText: String?, selectedRange: NSRange) {
|
|
1886
1913
|
ensureInternalTextViewDelegate()
|
|
1887
|
-
|
|
1914
|
+
if markedText != nil {
|
|
1915
|
+
captureMarkedTextReplacementRangeIfNeeded()
|
|
1916
|
+
}
|
|
1917
|
+
isComposing = markedText != nil || markedTextReplacementScalarRange != nil
|
|
1888
1918
|
Self.inputLog.debug(
|
|
1889
1919
|
"[setMarkedText] marked=\(self.preview(markedText ?? ""), privacy: .public) nsRange=\(selectedRange.location),\(selectedRange.length) selection=\(self.selectionSummary(), privacy: .public)"
|
|
1890
1920
|
)
|
|
1891
|
-
|
|
1921
|
+
performTransientTextMutation {
|
|
1922
|
+
super.setMarkedText(markedText, selectedRange: selectedRange)
|
|
1923
|
+
}
|
|
1924
|
+
if markedText == nil {
|
|
1925
|
+
clearMarkedTextTracking()
|
|
1926
|
+
restoreAuthorizedTextAfterCancelledCompositionIfNeeded()
|
|
1927
|
+
} else {
|
|
1928
|
+
refreshMarkedTextCompositionText(fallback: markedText)
|
|
1929
|
+
}
|
|
1892
1930
|
}
|
|
1893
1931
|
|
|
1894
1932
|
/// Called when composition is finalized (user selects a candidate or
|
|
1895
1933
|
/// presses space/enter to commit).
|
|
1896
1934
|
///
|
|
1897
|
-
/// At this point, the composed text is final. We capture it and
|
|
1898
|
-
///
|
|
1899
|
-
///
|
|
1900
|
-
/// but we intercept at a higher level.
|
|
1935
|
+
/// At this point, the composed text is final. We capture it and commit it
|
|
1936
|
+
/// to Rust at the original replacement range captured before UIKit mutated
|
|
1937
|
+
/// the transient text storage.
|
|
1901
1938
|
override func unmarkText() {
|
|
1902
1939
|
ensureInternalTextViewDelegate()
|
|
1903
|
-
|
|
1904
|
-
|
|
1940
|
+
let composedText = currentMarkedTextForCommit()
|
|
1941
|
+
let replacementRange = trackedMarkedTextReplacementRange()
|
|
1905
1942
|
|
|
1906
|
-
|
|
1907
|
-
// the Rust document doesn't have the composed text yet.
|
|
1908
|
-
isApplyingRustState = true
|
|
1909
|
-
super.unmarkText()
|
|
1910
|
-
isApplyingRustState = false
|
|
1911
|
-
isComposing = false
|
|
1943
|
+
finishTransientMarkedTextMutation()
|
|
1912
1944
|
|
|
1913
|
-
|
|
1914
|
-
// of the composed text, so the insert position is cursor - length.
|
|
1915
|
-
if let composed = composedText, !composed.isEmpty, editorId != 0 {
|
|
1916
|
-
let cursorPos = PositionBridge.cursorScalarOffset(in: self)
|
|
1917
|
-
let composedScalars = UInt32(composed.unicodeScalars.count)
|
|
1918
|
-
let insertPos = cursorPos >= composedScalars ? cursorPos - composedScalars : 0
|
|
1945
|
+
if let composed = composedText, !composed.isEmpty {
|
|
1919
1946
|
Self.inputLog.debug(
|
|
1920
|
-
"[unmarkText] composed=\(self.preview(composed), privacy: .public)
|
|
1947
|
+
"[unmarkText] composed=\(self.preview(composed), privacy: .public) replacement=\(self.previewMarkedTextReplacementRange(replacementRange), privacy: .public) selection=\(self.selectionSummary(), privacy: .public)"
|
|
1921
1948
|
)
|
|
1949
|
+
commitMarkedText(composed, replacementRange: replacementRange)
|
|
1950
|
+
} else {
|
|
1951
|
+
restoreAuthorizedTextAfterCancelledCompositionIfNeeded()
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
private func captureMarkedTextReplacementRangeIfNeeded() {
|
|
1956
|
+
guard markedTextReplacementScalarRange == nil else { return }
|
|
1957
|
+
|
|
1958
|
+
guard let selectedRange = selectedTextRange else {
|
|
1959
|
+
let scalarPos = PositionBridge.cursorScalarOffset(in: self)
|
|
1960
|
+
markedTextReplacementScalarRange = (from: scalarPos, to: scalarPos)
|
|
1961
|
+
markedTextReplacementUtf16Range = NSRange(
|
|
1962
|
+
location: Int(scalarPos),
|
|
1963
|
+
length: 0
|
|
1964
|
+
)
|
|
1965
|
+
return
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
let scalarRange = PositionBridge.textRangeToScalarRange(selectedRange, in: self)
|
|
1969
|
+
let startUtf16 = offset(from: beginningOfDocument, to: selectedRange.start)
|
|
1970
|
+
let endUtf16 = offset(from: beginningOfDocument, to: selectedRange.end)
|
|
1971
|
+
|
|
1972
|
+
markedTextReplacementScalarRange = (from: scalarRange.from, to: scalarRange.to)
|
|
1973
|
+
markedTextReplacementUtf16Range = NSRange(
|
|
1974
|
+
location: min(startUtf16, endUtf16),
|
|
1975
|
+
length: abs(endUtf16 - startUtf16)
|
|
1976
|
+
)
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
private func trackedMarkedTextReplacementRange() -> (from: UInt32, to: UInt32)? {
|
|
1980
|
+
if let markedTextReplacementScalarRange {
|
|
1981
|
+
return markedTextReplacementScalarRange
|
|
1982
|
+
}
|
|
1983
|
+
guard let selectedRange = selectedTextRange else { return nil }
|
|
1984
|
+
let scalarRange = PositionBridge.textRangeToScalarRange(selectedRange, in: self)
|
|
1985
|
+
return (from: scalarRange.from, to: scalarRange.to)
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
private func clearMarkedTextTracking() {
|
|
1989
|
+
markedTextReplacementScalarRange = nil
|
|
1990
|
+
markedTextReplacementUtf16Range = nil
|
|
1991
|
+
markedTextCompositionText = nil
|
|
1992
|
+
isComposing = false
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
private func finishTransientMarkedTextMutation() {
|
|
1996
|
+
performTransientTextMutation {
|
|
1997
|
+
super.unmarkText()
|
|
1998
|
+
}
|
|
1999
|
+
clearMarkedTextTracking()
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
private func performTransientTextMutation(_ action: () -> Void) {
|
|
2003
|
+
let wasApplyingRustState = isApplyingRustState
|
|
2004
|
+
isApplyingRustState = true
|
|
2005
|
+
action()
|
|
2006
|
+
isApplyingRustState = wasApplyingRustState
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
private func currentMarkedTextForCommit() -> String? {
|
|
2010
|
+
markedTextRange.flatMap { text(in: $0) }
|
|
2011
|
+
?? markedTextCompositionText
|
|
2012
|
+
?? transientMarkedTextFromAuthorizedDiff()
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
private func refreshMarkedTextCompositionText(fallback: String? = nil) {
|
|
2016
|
+
markedTextCompositionText = markedTextRange.flatMap { text(in: $0) }
|
|
2017
|
+
?? transientMarkedTextFromAuthorizedDiff()
|
|
2018
|
+
?? fallback
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
private func transientMarkedTextFromAuthorizedDiff() -> String? {
|
|
2022
|
+
guard let replacementRange = markedTextReplacementUtf16Range else { return nil }
|
|
2023
|
+
|
|
2024
|
+
let currentText = textStorage.string as NSString
|
|
2025
|
+
let authorizedText = lastAuthorizedText as NSString
|
|
2026
|
+
let replacementEnd = replacementRange.location + replacementRange.length
|
|
2027
|
+
guard replacementRange.location >= 0,
|
|
2028
|
+
replacementEnd <= authorizedText.length
|
|
2029
|
+
else {
|
|
2030
|
+
return nil
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
let insertedLength = currentText.length - (authorizedText.length - replacementRange.length)
|
|
2034
|
+
guard insertedLength >= 0,
|
|
2035
|
+
replacementRange.location + insertedLength <= currentText.length
|
|
2036
|
+
else {
|
|
2037
|
+
return nil
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
return currentText.substring(
|
|
2041
|
+
with: NSRange(location: replacementRange.location, length: insertedLength)
|
|
2042
|
+
)
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
private func commitMarkedText(
|
|
2046
|
+
_ text: String,
|
|
2047
|
+
replacementRange: (from: UInt32, to: UInt32)?
|
|
2048
|
+
) {
|
|
2049
|
+
guard editorId != 0 else { return }
|
|
2050
|
+
guard let replacementRange else {
|
|
1922
2051
|
performInterceptedInput {
|
|
1923
|
-
insertTextInRust(
|
|
2052
|
+
insertTextInRust(text, at: PositionBridge.cursorScalarOffset(in: self))
|
|
2053
|
+
}
|
|
2054
|
+
return
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
performInterceptedInput {
|
|
2058
|
+
if replacementRange.from == replacementRange.to {
|
|
2059
|
+
insertTextInRust(text, at: replacementRange.from)
|
|
2060
|
+
} else {
|
|
2061
|
+
replaceTextRangeInRust(
|
|
2062
|
+
from: replacementRange.from,
|
|
2063
|
+
to: replacementRange.to,
|
|
2064
|
+
with: text
|
|
2065
|
+
)
|
|
1924
2066
|
}
|
|
1925
2067
|
}
|
|
1926
|
-
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
private func restoreAuthorizedTextAfterCancelledCompositionIfNeeded() {
|
|
2071
|
+
guard editorId != 0 else { return }
|
|
2072
|
+
guard textStorage.string != lastAuthorizedText else { return }
|
|
2073
|
+
|
|
2074
|
+
let stateJSON = editorGetCurrentState(id: editorId)
|
|
2075
|
+
applyUpdateJSON(stateJSON)
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
private func previewMarkedTextReplacementRange(
|
|
2079
|
+
_ range: (from: UInt32, to: UInt32)?
|
|
2080
|
+
) -> String {
|
|
2081
|
+
guard let range else { return "none" }
|
|
2082
|
+
let utf16 = markedTextReplacementUtf16Range
|
|
2083
|
+
.map { "\($0.location)..<\($0.location + $0.length)" }
|
|
2084
|
+
?? "none"
|
|
2085
|
+
return "scalar=\(range.from)..<\(range.to) utf16=\(utf16)"
|
|
1927
2086
|
}
|
|
1928
2087
|
|
|
1929
2088
|
// MARK: - Paste Handling
|
|
@@ -1993,7 +2152,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1993
2152
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
|
1994
2153
|
guard textView === self else { return }
|
|
1995
2154
|
ensureInternalTextViewDelegate()
|
|
1996
|
-
guard !isApplyingRustState else { return }
|
|
2155
|
+
guard !isApplyingRustState, !isComposing else { return }
|
|
1997
2156
|
if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
|
|
1998
2157
|
return
|
|
1999
2158
|
}
|
|
@@ -2174,7 +2333,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2174
2333
|
}
|
|
2175
2334
|
|
|
2176
2335
|
private func syncSelectionToRustAndNotifyDelegate() {
|
|
2177
|
-
guard !isApplyingRustState, editorId != 0 else { return }
|
|
2336
|
+
guard !isApplyingRustState, !isComposing, editorId != 0 else { return }
|
|
2178
2337
|
guard let range = selectedTextRange else { return }
|
|
2179
2338
|
|
|
2180
2339
|
let anchor = PositionBridge.textViewToScalar(range.start, in: self)
|
|
@@ -2768,6 +2927,19 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2768
2927
|
applyUpdateJSON(updateJSON)
|
|
2769
2928
|
}
|
|
2770
2929
|
|
|
2930
|
+
private func replaceTextRangeInRust(from: UInt32, to: UInt32, with text: String) {
|
|
2931
|
+
Self.inputLog.debug(
|
|
2932
|
+
"[rust.replaceTextScalar] text=\(self.preview(text), privacy: .public) scalar=\(from)-\(to) selection=\(self.selectionSummary(), privacy: .public)"
|
|
2933
|
+
)
|
|
2934
|
+
let updateJSON = editorReplaceTextScalar(
|
|
2935
|
+
id: editorId,
|
|
2936
|
+
scalarFrom: from,
|
|
2937
|
+
scalarTo: to,
|
|
2938
|
+
text: text
|
|
2939
|
+
)
|
|
2940
|
+
applyUpdateJSON(updateJSON)
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2771
2943
|
private func insertNodeInRust(_ nodeType: String) {
|
|
2772
2944
|
guard let selection = currentScalarSelection() else { return }
|
|
2773
2945
|
Self.inputLog.debug(
|
|
@@ -4205,7 +4377,7 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
4205
4377
|
guard editedMask.contains(.editedCharacters) else { return }
|
|
4206
4378
|
|
|
4207
4379
|
// Skip if this change came from our own Rust apply path.
|
|
4208
|
-
guard !isApplyingRustState, !isInterceptingInput else { return }
|
|
4380
|
+
guard !isApplyingRustState, !isInterceptingInput, !isComposing else { return }
|
|
4209
4381
|
|
|
4210
4382
|
// Skip if no editor is bound yet (nothing to reconcile against).
|
|
4211
4383
|
guard editorId != 0 else { return }
|
|
@@ -4247,7 +4419,7 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
4247
4419
|
guard let self else { return }
|
|
4248
4420
|
self.reconciliationWorkScheduled = false
|
|
4249
4421
|
|
|
4250
|
-
guard !self.isApplyingRustState, !self.isInterceptingInput else { return }
|
|
4422
|
+
guard !self.isApplyingRustState, !self.isInterceptingInput, !self.isComposing else { return }
|
|
4251
4423
|
guard self.editorId != 0 else { return }
|
|
4252
4424
|
guard self.textStorage.string != self.lastAuthorizedText else { return }
|
|
4253
4425
|
|
|
@@ -4497,6 +4669,13 @@ final class RichTextEditorView: UIView {
|
|
|
4497
4669
|
remoteSelectionOverlayView.refresh()
|
|
4498
4670
|
}
|
|
4499
4671
|
|
|
4672
|
+
func currentCaretRect() -> CGRect? {
|
|
4673
|
+
guard let selectedTextRange = textView.selectedTextRange else { return nil }
|
|
4674
|
+
let rect = textView.caretRect(for: selectedTextRange.end)
|
|
4675
|
+
guard rect.height > 0 else { return nil }
|
|
4676
|
+
return textView.convert(rect, to: self)
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4500
4679
|
func remoteSelectionOverlaySubviewsForTesting() -> [UIView] {
|
|
4501
4680
|
remoteSelectionOverlayView.subviews.filter { !$0.isHidden }
|
|
4502
4681
|
}
|
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.11",
|
|
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
|